Index: branches/salvatoreingala/Gadgets/Gadgets.i18n.php |
— | — | @@ -61,6 +61,21 @@ |
62 | 62 | 'gadgets-formbuilder-integer' => 'Please enter an integer number.', |
63 | 63 | 'gadgets-formbuilder-date' => 'Please enter a valid date.', |
64 | 64 | 'gadgets-formbuilder-color' => 'Please enter a valid color.', |
| 65 | + 'gadgets-formbuilder-editor-ok' => 'OK', |
| 66 | + 'gadgets-formbuilder-editor-cancel' => 'Cancel', |
| 67 | + 'gadgets-formbuilder-editor-move-field' => 'Move this field', |
| 68 | + 'gadgets-formbuilder-editor-delete-field' => 'Delete this field', |
| 69 | + 'gadgets-formbuilder-editor-edit-field' => 'Edit field properties', |
| 70 | + 'gadgets-formbuilder-editor-insert-field' => 'Insert a new field', |
| 71 | + 'gadgets-formbuilder-editor-chose-field' => 'Chose the type of the new field:', |
| 72 | + 'gadgets-formbuilder-editor-chose-field-title' => 'Chose field type', |
| 73 | + 'gadgets-formbuilder-editor-create-field-title' => 'Create field', |
| 74 | + 'gadgets-formbuilder-editor-duplicate-name' => 'The preference name $1 has been used. Please chose a different name.', |
| 75 | + 'gadgets-formbuilder-editor-edit-section' => 'Edit this section\'s title', |
| 76 | + 'gadgets-formbuilder-editor-delete-section' => 'Delete this section and all his content', |
| 77 | + 'gadgets-formbuilder-editor-new-section' => 'Delete this section and all his content', |
| 78 | + 'gadgets-formbuilder-editor-chose-title' => 'Chose the title of the new section:', |
| 79 | + 'gadgets-formbuilder-editor-chose-title-title' => 'Chose section title', |
65 | 80 | ); |
66 | 81 | |
67 | 82 | /** Message documentation (Message documentation) |
Index: branches/salvatoreingala/Gadgets/Gadgets.php |
— | — | @@ -86,12 +86,18 @@ |
87 | 87 | 'styles' => array( 'jquery.formBuilder.css' ), |
88 | 88 | 'dependencies' => array( |
89 | 89 | 'jquery', 'jquery.ui.slider', 'jquery.ui.datepicker', 'jquery.ui.position', |
| 90 | + 'jquery.ui.draggable', 'jquery.ui.droppable', 'jquery.ui.sortable', 'jquery.ui.dialog', |
90 | 91 | 'jquery.ui.tabs', 'jquery.farbtastic', 'jquery.colorUtil', 'jquery.validate' |
91 | 92 | ), |
92 | 93 | 'messages' => array( |
93 | 94 | 'gadgets-formbuilder-required', 'gadgets-formbuilder-minlength', 'gadgets-formbuilder-maxlength', |
94 | 95 | 'gadgets-formbuilder-min', 'gadgets-formbuilder-max', 'gadgets-formbuilder-integer', 'gadgets-formbuilder-date', |
95 | | - 'gadgets-formbuilder-color' |
| 96 | + 'gadgets-formbuilder-color', |
| 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', |
| 99 | + 'gadgets-formbuilder-editor-chose-field', 'gadgets-formbuilder-editor-chose-field-title', 'gadgets-formbuilder-editor-create-field-title', |
| 100 | + 'gadgets-formbuilder-editor-duplicate-name', 'gadgets-formbuilder-editor-delete-section', 'gadgets-formbuilder-editor-new-section', |
| 101 | + 'gadgets-formbuilder-editor-chose-title', 'gadgets-formbuilder-editor-chose-title-title' |
96 | 102 | ), |
97 | 103 | 'localBasePath' => $dir . 'ui/resources/', |
98 | 104 | 'remoteExtPath' => 'Gadgets/ui/resources' |
Index: branches/salvatoreingala/Gadgets/Gadgets_tests.php |
— | — | @@ -342,10 +342,10 @@ |
343 | 343 | 'label' => 'some label', |
344 | 344 | 'default' => 3, |
345 | 345 | 'options' => array( |
346 | | - 'opt1' => null, |
347 | | - 'opt2' => true, |
348 | | - 'opt3' => 3, |
349 | | - 'opt4' => 'test' |
| 346 | + array( 'name' => 'opt1', 'value' => null ), |
| 347 | + array( 'name' => 'opt2', 'value' => true ), |
| 348 | + array( 'name' => 'opt3', 'value' => 3 ), |
| 349 | + array( 'name' => 'opt4', 'value' => 'test' ) |
350 | 350 | ) |
351 | 351 | ) |
352 | 352 | ) |
— | — | @@ -536,7 +536,8 @@ |
537 | 537 | array( |
538 | 538 | 'type' => 'bundle', |
539 | 539 | 'sections' => array( |
540 | | - '@section1' => array( |
| 540 | + array( |
| 541 | + 'title' => '@section1', |
541 | 542 | 'fields' => array ( |
542 | 543 | array( |
543 | 544 | 'name' => 'testBoolean', |
— | — | @@ -560,7 +561,8 @@ |
561 | 562 | ) |
562 | 563 | ) |
563 | 564 | ), |
564 | | - 'Section2' => array( |
| 565 | + array( |
| 566 | + 'title' => 'Section2', |
565 | 567 | 'fields' => array( |
566 | 568 | array( |
567 | 569 | 'name' => 'testNumber2', |
— | — | @@ -576,10 +578,10 @@ |
577 | 579 | 'label' => 'foo', |
578 | 580 | 'default' => 3, |
579 | 581 | 'options' => array( |
580 | | - '@opt1' => null, |
581 | | - '@opt2' => true, |
582 | | - 'opt3' => 3, |
583 | | - '@opt4' => 'opt4value' |
| 582 | + array( 'name' => '@opt1', 'value' => null ), |
| 583 | + array( 'name' => '@opt2', 'value' => true ), |
| 584 | + array( 'name' => 'opt3', 'value' => 3 ), |
| 585 | + array( 'name' => '@opt4', 'value' => 'opt4value' ) |
584 | 586 | ) |
585 | 587 | ), |
586 | 588 | array( |
— | — | @@ -588,10 +590,10 @@ |
589 | 591 | 'label' => 'foo', |
590 | 592 | 'default' => 3, |
591 | 593 | 'options' => array( |
592 | | - '@opt1' => null, |
593 | | - 'opt2' => true, |
594 | | - 'opt3' => 3, |
595 | | - 'opt4' => 'opt4value' |
| 594 | + array( 'name' => '@opt1', 'value' => null ), |
| 595 | + array( 'name' => 'opt2', 'value' => true ), |
| 596 | + array( 'name' => 'opt3', 'value' => 3 ), |
| 597 | + array( 'name' => 'opt4', 'value' => 'opt4value' ) |
596 | 598 | ) |
597 | 599 | ) |
598 | 600 | ) |
Index: branches/salvatoreingala/Gadgets/backend/GadgetPrefs.php |
— | — | @@ -74,7 +74,7 @@ |
75 | 75 | 'validator' => 'is_integer' |
76 | 76 | ) |
77 | 77 | ), |
78 | | - 'validator' => 'GadgetPrefs::validateStringOptionDefinition', |
| 78 | + 'validator' => 'GadgetPrefs::validateStringPrefDefinition', |
79 | 79 | 'checker' => 'GadgetPrefs::checkStringPref' |
80 | 80 | ), |
81 | 81 | 'number' => array( |
— | — | @@ -108,7 +108,7 @@ |
109 | 109 | 'validator' => 'GadgetPrefs::isFloatOrInt' |
110 | 110 | ) |
111 | 111 | ), |
112 | | - 'validator' => 'GadgetPrefs::validateNumberOptionDefinition', |
| 112 | + 'validator' => 'GadgetPrefs::validateNumberPrefDefinition', |
113 | 113 | 'checker' => 'GadgetPrefs::checkNumberPref' |
114 | 114 | ), |
115 | 115 | 'select' => array( |
— | — | @@ -126,10 +126,10 @@ |
127 | 127 | ), |
128 | 128 | 'options' => array( |
129 | 129 | 'isMandatory' => true, |
130 | | - 'validator' => 'is_array' |
| 130 | + 'validator' => 'GadgetPrefs::isOrdinaryArray' |
131 | 131 | ) |
132 | 132 | ), |
133 | | - 'validator' => 'GadgetPrefs::validateSelectOptionDefinition', |
| 133 | + 'validator' => 'GadgetPrefs::validateSelectPrefDefinition', |
134 | 134 | 'checker' => 'GadgetPrefs::checkSelectPref', |
135 | 135 | 'getMessages' => 'GadgetPrefs::getSelectMessages' |
136 | 136 | ), |
— | — | @@ -160,7 +160,7 @@ |
161 | 161 | 'validator' => 'GadgetPrefs::isFloatOrInt' |
162 | 162 | ) |
163 | 163 | ), |
164 | | - 'validator' => 'GadgetPrefs::validateRangeOptionDefinition', |
| 164 | + 'validator' => 'GadgetPrefs::validateRangePrefDefinition', |
165 | 165 | 'checker' => 'GadgetPrefs::checkRangePref' |
166 | 166 | ), |
167 | 167 | 'date' => array( |
— | — | @@ -214,17 +214,17 @@ |
215 | 215 | |
216 | 216 | |
217 | 217 | //Further checks for 'string' options |
218 | | - private static function validateStringOptionDefinition( $option ) { |
219 | | - if ( isset( $option['minlength'] ) && $option['minlength'] < 0 ) { |
| 218 | + private static function validateStringPrefDefinition( $prefDefinition ) { |
| 219 | + if ( isset( $prefDefinition['minlength'] ) && $prefDefinition['minlength'] < 0 ) { |
220 | 220 | return false; |
221 | 221 | } |
222 | 222 | |
223 | | - if ( isset( $option['maxlength'] ) && $option['maxlength'] <= 0 ) { |
| 223 | + if ( isset( $prefDefinition['maxlength'] ) && $prefDefinition['maxlength'] <= 0 ) { |
224 | 224 | return false; |
225 | 225 | } |
226 | 226 | |
227 | | - if ( isset( $option['minlength']) && isset( $option['maxlength'] ) ) { |
228 | | - if ( $option['minlength'] > $option['maxlength'] ) { |
| 227 | + if ( isset( $prefDefinition['minlength']) && isset( $prefDefinition['maxlength'] ) ) { |
| 228 | + if ( $prefDefinition['minlength'] > $prefDefinition['maxlength'] ) { |
229 | 229 | return false; |
230 | 230 | } |
231 | 231 | } |
— | — | @@ -240,6 +240,16 @@ |
241 | 241 | return is_float( $param ) || is_int( $param ) || $param === null; |
242 | 242 | } |
243 | 243 | |
| 244 | + //Checks if $param is an ordinary (i.e.: not associative) array |
| 245 | + private static function isOrdinaryArray( $param ) { |
| 246 | + if ( !is_array( $param ) ) { |
| 247 | + return false; |
| 248 | + } |
| 249 | + |
| 250 | + $count = count( $param ); |
| 251 | + return $count == 0 || array_keys( $param ) === range( 0, $count - 1 ); |
| 252 | + } |
| 253 | + |
244 | 254 | //default flattener for simple fields that encode for a single preference |
245 | 255 | private static function flattenSimpleField( $fieldDescription ) { |
246 | 256 | return array( $fieldDescription['name'] => $fieldDescription ); |
— | — | @@ -248,7 +258,7 @@ |
249 | 259 | //flattener for 'bundle' fields |
250 | 260 | private static function flattenBundleDefinition( $fieldDescription ) { |
251 | 261 | $flattenedPrefs = array(); |
252 | | - foreach ( $fieldDescription['sections'] as $sectionName => $sectionDescription ) { |
| 262 | + foreach ( $fieldDescription['sections'] as $sectionDescription ) { |
253 | 263 | //Each section behaves like a full description of preferences |
254 | 264 | $flt = self::flattenPrefsDescription( $sectionDescription ); |
255 | 265 | $flattenedPrefs = array_merge( $flattenedPrefs, $flt ); |
— | — | @@ -257,16 +267,16 @@ |
258 | 268 | } |
259 | 269 | |
260 | 270 | //Further checks for 'number' options |
261 | | - private static function validateNumberOptionDefinition( $option ) { |
262 | | - if ( isset( $option['integer'] ) && $option['integer'] === true ) { |
| 271 | + private static function validateNumberPrefDefinition( $prefDefinition ) { |
| 272 | + if ( isset( $prefDefinition['integer'] ) && $prefDefinition['integer'] === true ) { |
263 | 273 | //Check if 'min', 'max' and 'default' are integers (if given) |
264 | | - if ( intval( $option['default'] ) != $option['default'] ) { |
| 274 | + if ( intval( $prefDefinition['default'] ) != $prefDefinition['default'] ) { |
265 | 275 | return false; |
266 | 276 | } |
267 | | - if ( isset( $option['min'] ) && intval( $option['min'] ) != $option['min'] ) { |
| 277 | + if ( isset( $prefDefinition['min'] ) && intval( $prefDefinition['min'] ) != $prefDefinition['min'] ) { |
268 | 278 | return false; |
269 | 279 | } |
270 | | - if ( isset( $option['max'] ) && intval( $option['max'] ) != $option['max'] ) { |
| 280 | + if ( isset( $prefDefinition['max'] ) && intval( $prefDefinition['max'] ) != $prefDefinition['max'] ) { |
271 | 281 | return false; |
272 | 282 | } |
273 | 283 | } |
— | — | @@ -274,35 +284,49 @@ |
275 | 285 | return true; |
276 | 286 | } |
277 | 287 | |
278 | | - private static function validateSelectOptionDefinition( $option ) { |
279 | | - $options = $option['options']; |
| 288 | + private static function validateSelectPrefDefinition( $prefDefinition ) { |
| 289 | + $options = $prefDefinition['options']; |
280 | 290 | |
281 | | - foreach ( $options as $opt => $optVal ) { |
282 | | - //Correct value for $optVal are NULL, boolean, integer, float or string |
283 | | - if ( $optVal !== NULL && |
284 | | - !is_bool( $optVal ) && |
285 | | - !is_int( $optVal ) && |
286 | | - !is_float( $optVal ) && |
287 | | - !is_string( $optVal ) ) |
| 291 | + //Check if it's a regular array |
| 292 | + if ( !self::isOrdinaryArray( $options ) ) { |
| 293 | + return false; |
| 294 | + } |
| 295 | + |
| 296 | + foreach ( $options as $option ) { |
| 297 | + //Using array_key_exists() because isset fails for null values |
| 298 | + if ( !isset( $option['name'] ) || !array_key_exists( 'value', $option ) ) { |
| 299 | + return false; |
| 300 | + } |
| 301 | + |
| 302 | + //All names must be strings |
| 303 | + if ( !is_string( $option['name'] ) ) { |
| 304 | + return false; |
| 305 | + } |
| 306 | + |
| 307 | + //Correct value for $value are null, boolean, integer, float or string |
| 308 | + $value = $option['value']; |
| 309 | + if ( $value !== null && |
| 310 | + !is_bool( $value ) && |
| 311 | + !is_int( $value ) && |
| 312 | + !is_float( $value ) && |
| 313 | + !is_string( $value ) ) |
288 | 314 | { |
289 | 315 | return false; |
290 | 316 | } |
291 | 317 | } |
292 | | - |
293 | | - $values = array_values( $options ); |
294 | | - |
| 318 | + |
295 | 319 | return true; |
296 | 320 | } |
297 | 321 | |
298 | | - private static function validateRangeOptionDefinition( $option ) { |
299 | | - $step = isset( $option['step'] ) ? $option['step'] : 1; |
| 322 | + private static function validateRangePrefDefinition( $prefDefinition ) { |
| 323 | + $step = isset( $prefDefinition['step'] ) ? $prefDefinition['step'] : 1; |
300 | 324 | |
301 | 325 | if ( $step <= 0 ) { |
302 | 326 | return false; |
303 | 327 | } |
304 | 328 | |
305 | | - $min = $option['min']; |
306 | | - $max = $option['max']; |
| 329 | + $min = $prefDefinition['min']; |
| 330 | + $max = $prefDefinition['max']; |
307 | 331 | |
308 | 332 | //Checks if 'max' is a valid value |
309 | 333 | //Valid values are min, min + step, min + 2*step, ... |
— | — | @@ -458,6 +482,10 @@ |
459 | 483 | //validate each section, then ensure that preference names |
460 | 484 | //of each section are disjoint |
461 | 485 | |
| 486 | + if ( !self::isOrdinaryArray( $sections ) ) { |
| 487 | + return false; |
| 488 | + } |
| 489 | + |
462 | 490 | $prefs = array(); //names of preferences |
463 | 491 | |
464 | 492 | foreach ( $sections as $section ) { |
— | — | @@ -465,6 +493,11 @@ |
466 | 494 | return false; |
467 | 495 | } |
468 | 496 | |
| 497 | + //Bundle sections must have a "title" field |
| 498 | + if ( !isset( $section['title'] ) || !is_istring( $section['title'] ) ) { |
| 499 | + return false; |
| 500 | + } |
| 501 | + |
469 | 502 | $flt = self::flattenPrefsDescription( $section ); |
470 | 503 | $newPrefs = array_keys( $flt ); |
471 | 504 | if ( array_intersect( $prefs, $newPrefs ) ) { |
— | — | @@ -582,8 +615,13 @@ |
583 | 616 | |
584 | 617 | //Checker for 'select' preferences |
585 | 618 | private static function checkSelectPref( $prefDescription, $value ) { |
586 | | - $values = array_values( $prefDescription['options'] ); |
587 | | - return in_array( $value, $values, true ); |
| 619 | + foreach ( $prefDescription['options'] as $option ) { |
| 620 | + if ( $option['value'] === $value ) { |
| 621 | + return true; |
| 622 | + } |
| 623 | + } |
| 624 | + |
| 625 | + return false; |
588 | 626 | } |
589 | 627 | |
590 | 628 | //Checker for 'range' preferences |
— | — | @@ -762,7 +800,8 @@ |
763 | 801 | //Returns the messages for a 'select' field description |
764 | 802 | private static function getSelectMessages( $prefDescription ) { |
765 | 803 | $msgs = array(); |
766 | | - foreach ( $prefDescription['options'] as $optName => $value ) { |
| 804 | + foreach ( $prefDescription['options'] as $option ) { |
| 805 | + $optName = $option['name']; |
767 | 806 | if ( self::isMessage( $optName ) ) { |
768 | 807 | $msgs[] = substr( $optName, 1 ); |
769 | 808 | } |
— | — | @@ -774,10 +813,11 @@ |
775 | 814 | private static function getBundleMessages( $prefDescription ) { |
776 | 815 | //returns the union of all messages of all sections, plus section names |
777 | 816 | $msgs = array(); |
778 | | - foreach ( $prefDescription['sections'] as $sectionName => $sectionDescription ) { |
| 817 | + foreach ( $prefDescription['sections'] as $sectionDescription ) { |
779 | 818 | $msgs = array_merge( $msgs, self::getMessages( $sectionDescription ) ); |
780 | | - if ( self::isMessage( $sectionName ) ) { |
781 | | - $msgs[] = substr( $sectionName, 1 ); |
| 819 | + $sectionTitle = $sectionDescription['title']; |
| 820 | + if ( self::isMessage( $sectionTitle ) ) { |
| 821 | + $msgs[] = substr( $sectionTitle, 1 ); |
782 | 822 | } |
783 | 823 | } |
784 | 824 | return array_unique( $msgs ); |
Index: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.css |
— | — | @@ -43,6 +43,65 @@ |
44 | 44 | } |
45 | 45 | |
46 | 46 | .farbtastic { |
47 | | - border: 1px solid #cccccc; |
| 47 | + border: none; |
48 | 48 | } |
49 | 49 | |
| 50 | +.formbuilder-slot { |
| 51 | + border: none; |
| 52 | + padding-top: 0.5em; |
| 53 | + padding-bottom: 0.5em; |
| 54 | +} |
| 55 | + |
| 56 | +/* formBuilder editor */ |
| 57 | + |
| 58 | +.formbuilder-slot-nonempty { |
| 59 | + border: 1px solid #888; |
| 60 | + padding: 0px; |
| 61 | +} |
| 62 | + |
| 63 | +.formbuilder-slot-empty { |
| 64 | + border: 1px dashed #aaa; |
| 65 | + padding: 0px; |
| 66 | +} |
| 67 | + |
| 68 | +.formbuilder-slot-can-drop { |
| 69 | + border: 1px solid black; |
| 70 | +} |
| 71 | + |
| 72 | +.formbuilder-editor-slot-buttons { |
| 73 | + height: 19px; |
| 74 | +} |
| 75 | + |
| 76 | +.formbuilder-editor-button { |
| 77 | + cursor: pointer; |
| 78 | +} |
| 79 | + |
| 80 | +.formbuilder-editor-button-move { |
| 81 | + cursor: move; |
| 82 | +} |
| 83 | + |
| 84 | +.formbuilder-editor-button-new, .formbuilder-editor-button-new-section { |
| 85 | + float: left; |
| 86 | +} |
| 87 | + |
| 88 | +.formbuilder-editor-button-edit { |
| 89 | + float: left; |
| 90 | +} |
| 91 | + |
| 92 | +.formbuilder-editor-button-move { |
| 93 | + float: left; |
| 94 | +} |
| 95 | + |
| 96 | +.formbuilder-editor-button-delete { |
| 97 | + float: right; |
| 98 | +} |
| 99 | + |
| 100 | +.formbuilder-editor-button-delete-section { |
| 101 | + float: right; |
| 102 | +} |
| 103 | + |
| 104 | +.formbuilder-editor-button-edit-section { |
| 105 | + float: right; |
| 106 | + margin-left: 6px; |
| 107 | +} |
| 108 | + |
Index: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.js |
— | — | @@ -12,13 +12,13 @@ |
13 | 13 | //If str starts with "@" the rest of the string is assumed to be |
14 | 14 | //a message, and the result of mw.msg is returned. |
15 | 15 | //Two "@@" at the beginning escape for a single "@". |
16 | | - function preproc( $form, str ) { |
| 16 | + function preproc( prefix, str ) { |
17 | 17 | if ( str.length <= 1 || str[0] !== '@' ) { |
18 | 18 | return str; |
19 | 19 | } else if ( str.substr( 0, 2 ) == '@@' ) { |
20 | 20 | return str.substr( 1 ); |
21 | 21 | } else { |
22 | | - return mw.message( $form.data( 'formBuilder' ).prefix + str.substring( 1 ) ).plain(); |
| 22 | + return mw.message( prefix + str.substr( 1 ) ).plain(); |
23 | 23 | } |
24 | 24 | } |
25 | 25 | |
— | — | @@ -38,6 +38,12 @@ |
39 | 39 | return res; |
40 | 40 | } |
41 | 41 | |
| 42 | + //Returns an object with only one key and the corresponding value given in arguments; |
| 43 | + function pair( key, val ) { |
| 44 | + var res = {}; |
| 45 | + res[key] = val; |
| 46 | + return res; |
| 47 | + } |
42 | 48 | |
43 | 49 | function testOptional( value, element ) { |
44 | 50 | var rules = $( element ).rules(); |
— | — | @@ -92,13 +98,98 @@ |
93 | 99 | return new F(); |
94 | 100 | } |
95 | 101 | |
| 102 | + |
| 103 | + //Field types that can be referred to by preference descriptions |
| 104 | + var validFieldTypes = {}; |
| 105 | + |
| 106 | + |
| 107 | + //Describes 'name' and 'label' field members |
| 108 | + var simpleField = [ |
| 109 | + { |
| 110 | + "name": "name", |
| 111 | + "type": "string", |
| 112 | + "label": "name", |
| 113 | + "required": true, |
| 114 | + "maxlength": 40, |
| 115 | + "default": "" |
| 116 | + }, |
| 117 | + { |
| 118 | + "name": "label", |
| 119 | + "type": "string", |
| 120 | + "label": "label", |
| 121 | + "required": false, |
| 122 | + "default": "" |
| 123 | + } |
| 124 | + ]; |
| 125 | + |
| 126 | + //Used by preference editor to build field properties dialogs |
| 127 | + var prefsDescriptionSpecifications = { |
| 128 | + "boolean": simpleField, |
| 129 | + "string" : simpleField.concat( [ |
| 130 | + { |
| 131 | + "name": "required", |
| 132 | + "type": "boolean", |
| 133 | + "label": "required", |
| 134 | + "default": false |
| 135 | + }, |
| 136 | + { |
| 137 | + "name": "minlength", |
| 138 | + "type": "number", |
| 139 | + "label": "minlength", |
| 140 | + "integer": true, |
| 141 | + "min": 0, |
| 142 | + "required": false, |
| 143 | + "default": null |
| 144 | + }, |
| 145 | + { |
| 146 | + "name": "maxlength", |
| 147 | + "type": "number", |
| 148 | + "label": "maxlength", |
| 149 | + "integer": true, |
| 150 | + "min": 0, |
| 151 | + "required": false, |
| 152 | + "default": null |
| 153 | + } |
| 154 | + ] ), |
| 155 | + "number" : simpleField.concat( [ |
| 156 | + { |
| 157 | + "name": "required", |
| 158 | + "type": "boolean", |
| 159 | + "label": "required", |
| 160 | + "default": true |
| 161 | + }, |
| 162 | + { |
| 163 | + "name": "integer", |
| 164 | + "type": "boolean", |
| 165 | + "label": "integer", |
| 166 | + "default": false |
| 167 | + }, |
| 168 | + { |
| 169 | + "name": "min", |
| 170 | + "type": "number", |
| 171 | + "label": "min", |
| 172 | + "required": false, |
| 173 | + "default": null |
| 174 | + }, |
| 175 | + { |
| 176 | + "name": "max", |
| 177 | + "type": "number", |
| 178 | + "label": "max", |
| 179 | + "required": false, |
| 180 | + "default": null |
| 181 | + } |
| 182 | + //TODO: other fields |
| 183 | + ] ) |
| 184 | + }; |
| 185 | + |
96 | 186 | /* Basic interface for fields */ |
97 | | - function Field( $form, desc, values ) { |
98 | | - this.$form = $form; |
| 187 | + function Field( desc, options ) { |
| 188 | + this.prefix = options.prefix; |
99 | 189 | this.desc = desc; |
| 190 | + this.options = options; |
100 | 191 | } |
101 | 192 | |
102 | | - Field.prototype.getDesc = function() { |
| 193 | + Field.prototype.getDesc = function( useValuesAsDefaults ) { |
103 | 194 | return this.desc; |
104 | 195 | }; |
105 | 196 | |
— | — | @@ -120,97 +211,121 @@ |
121 | 212 | }; |
122 | 213 | }; |
123 | 214 | |
124 | | - |
125 | 215 | /* A field with no content, generating an empty container */ |
126 | 216 | EmptyField.prototype = object( Field.prototype ); |
127 | 217 | EmptyField.prototype.constructor = EmptyField; |
128 | | - function EmptyField( $form, desc, values ) { |
129 | | - Field.call( this, $form, desc, values ); |
| 218 | + function EmptyField( desc, options ) { |
| 219 | + Field.call( this, desc, options ); |
130 | 220 | |
131 | 221 | //Check existence and type of the "type" field |
132 | | - if ( !desc.type || typeof desc.type != 'string' ) { |
| 222 | + if ( !this.desc.type || typeof this.desc.type != 'string' ) { |
133 | 223 | $.error( "Missing 'type' parameter" ); |
134 | 224 | } |
135 | 225 | |
136 | | - this.$p = $( '<p/>' ); |
| 226 | + this.$div = $( '<div/>' ).data( 'field', this ); |
137 | 227 | } |
138 | 228 | |
139 | 229 | EmptyField.prototype.getElement = function() { |
140 | | - return this.$p; |
| 230 | + return this.$div; |
141 | 231 | }; |
142 | 232 | |
143 | 233 | /* A field with just a label */ |
144 | 234 | LabelField.prototype = object( EmptyField.prototype ); |
145 | 235 | LabelField.prototype.constructor = LabelField; |
146 | | - function LabelField( $form, desc, values ) { |
147 | | - EmptyField.call( this, $form, desc, values ); |
| 236 | + function LabelField( desc, options ) { |
| 237 | + EmptyField.call( this, desc, options ); |
148 | 238 | |
149 | 239 | //Check existence and type of the "label" field |
150 | | - if ( !desc.label || typeof desc.label != 'string' ) { |
151 | | - $.error( "Missing 'label' parameter" ); |
| 240 | + if ( typeof this.desc.label != 'string' ) { |
| 241 | + $.error( "Missing or wrong 'label' parameter" ); |
152 | 242 | } |
153 | 243 | |
154 | 244 | var $label = $( '<label/>' ) |
155 | | - .text( preproc( this.$form, this.desc.label ) ) |
| 245 | + .text( preproc( this.prefix, this.desc.label ) ) |
156 | 246 | .attr('for', idPrefix + this.desc.name ); |
157 | 247 | |
158 | | - this.$p.append( $label ); |
| 248 | + this.$div.append( $label ); |
159 | 249 | } |
160 | 250 | |
| 251 | + /* Abstract base class for all "simple" fields. Should not be instantiated. */ |
| 252 | + SimpleField.prototype = object( LabelField.prototype ); |
| 253 | + SimpleField.prototype.constructor = SimpleField; |
| 254 | + function SimpleField( desc, options ){ |
| 255 | + LabelField.call( this, desc, options ); |
| 256 | + } |
| 257 | + |
| 258 | + SimpleField.prototype.getDesc = function( useValuesAsDefaults ) { |
| 259 | + var desc = LabelField.prototype.getDesc.call( this, useValuesAsDefaults ); |
| 260 | + if ( useValuesAsDefaults === true ) { |
| 261 | + //set 'default' to current value. |
| 262 | + var values = this.getValues(); |
| 263 | + desc['default'] = values[this.desc.name]; |
| 264 | + } |
| 265 | + |
| 266 | + return desc; |
| 267 | + }; |
| 268 | + |
| 269 | + |
161 | 270 | /* A field with a label and a checkbox */ |
162 | | - BooleanField.prototype = object( LabelField.prototype ); |
| 271 | + BooleanField.prototype = object( SimpleField.prototype ); |
163 | 272 | BooleanField.prototype.constructor = BooleanField; |
164 | | - function BooleanField( $form, desc, values ){ |
165 | | - LabelField.call( this, $form, desc, values ); |
| 273 | + function BooleanField( desc, options ){ |
| 274 | + SimpleField.call( this, desc, options ); |
166 | 275 | |
167 | | - var value = values[this.desc.name]; |
168 | | - if ( typeof value != 'boolean' ) { |
169 | | - $.error( "value is invalid" ); |
| 276 | + this.$c = $( '<input/>' ).attr( { |
| 277 | + type: 'checkbox', |
| 278 | + id: idPrefix + this.desc.name, |
| 279 | + name: idPrefix + this.desc.name |
| 280 | + } ); |
| 281 | + |
| 282 | + var value = options.values && options.values[this.desc.name]; |
| 283 | + if ( typeof value != 'undefined' ) { |
| 284 | + if ( typeof value != 'boolean' ) { |
| 285 | + $.error( "value is invalid" ); |
| 286 | + } |
| 287 | + |
| 288 | + this.$c.attr( 'checked', value ); |
170 | 289 | } |
171 | | - |
172 | | - this.$c = $( '<input/>' ) |
173 | | - .attr( 'type', 'checkbox' ) |
174 | | - .attr( 'id', idPrefix + this.desc.name ) |
175 | | - .attr( 'name', idPrefix + this.desc.name ) |
176 | | - .attr( 'checked', value ); |
177 | 290 | |
178 | | - this.$p.append( this.$c ); |
| 291 | + this.$div.append( this.$c ); |
179 | 292 | } |
180 | 293 | |
181 | 294 | BooleanField.prototype.getValues = function() { |
182 | | - var res = {}; |
183 | | - res[this.desc.name] = this.$c.is( ':checked' ); |
184 | | - return res; |
| 295 | + return pair( this.desc.name, this.$c.is( ':checked' ) ); |
185 | 296 | }; |
186 | 297 | |
| 298 | + validFieldTypes["boolean"] = BooleanField; |
| 299 | + |
187 | 300 | /* A field with a textbox accepting string values */ |
188 | | - StringField.prototype = object( LabelField.prototype ); |
| 301 | + StringField.prototype = object( SimpleField.prototype ); |
189 | 302 | StringField.prototype.constructor = StringField; |
190 | | - function StringField( $form, desc, values ){ |
191 | | - LabelField.call( this, $form, desc, values ); |
| 303 | + function StringField( desc, options ){ |
| 304 | + SimpleField.call( this, desc, options ); |
192 | 305 | |
193 | | - var value = values[this.desc.name]; |
194 | | - if ( typeof value != 'string' ) { |
195 | | - $.error( "value is invalid" ); |
| 306 | + this.$text = $( '<input/>' ).attr( { |
| 307 | + type: 'text', |
| 308 | + id: idPrefix + this.desc.name, |
| 309 | + name: idPrefix + this.desc.name |
| 310 | + } ); |
| 311 | + |
| 312 | + var value = options.values && options.values[this.desc.name]; |
| 313 | + if ( typeof value != 'undefined' ) { |
| 314 | + if ( typeof value != 'string' ) { |
| 315 | + $.error( "value is invalid" ); |
| 316 | + } |
| 317 | + |
| 318 | + this.$text.val( value ); |
196 | 319 | } |
197 | 320 | |
198 | | - this.$text = $( '<input/>' ) |
199 | | - .attr( 'type', 'text' ) |
200 | | - .attr( 'id', idPrefix + this.desc.name ) |
201 | | - .attr( 'name', idPrefix + this.desc.name ) |
202 | | - .val( value ); |
203 | | - |
204 | | - this.$p.append( this.$text ); |
| 321 | + this.$div.append( this.$text ); |
205 | 322 | } |
206 | 323 | |
207 | 324 | StringField.prototype.getValues = function() { |
208 | | - var res = {}; |
209 | | - res[this.desc.name] = this.$text.val(); |
210 | | - return res; |
| 325 | + return pair( this.desc.name, this.$text.val() ); |
211 | 326 | }; |
212 | 327 | |
213 | 328 | StringField.prototype.getValidationSettings = function() { |
214 | | - var settings = LabelField.prototype.getValidationSettings.call( this ), |
| 329 | + var settings = SimpleField.prototype.getValidationSettings.call( this ), |
215 | 330 | fieldId = idPrefix + this.desc.name; |
216 | 331 | |
217 | 332 | settings.rules[fieldId] = {}; |
— | — | @@ -238,35 +353,40 @@ |
239 | 354 | return settings; |
240 | 355 | }; |
241 | 356 | |
| 357 | + validFieldTypes["string"] = StringField; |
| 358 | + |
| 359 | + |
242 | 360 | /* A field with a textbox accepting numeric values */ |
243 | | - NumberField.prototype = object( LabelField.prototype ); |
| 361 | + NumberField.prototype = object( SimpleField.prototype ); |
244 | 362 | NumberField.prototype.constructor = NumberField; |
245 | | - function NumberField( $form, desc, values ){ |
246 | | - LabelField.call( this, $form, desc, values ); |
| 363 | + function NumberField( desc, options ){ |
| 364 | + SimpleField.call( this, desc, options ); |
247 | 365 | |
248 | | - var value = values[this.desc.name]; |
249 | | - if ( value !== null && typeof value != 'number' ) { |
250 | | - $.error( "value is invalid" ); |
| 366 | + this.$text = $( '<input/>' ).attr( { |
| 367 | + type: 'text', |
| 368 | + id: idPrefix + this.desc.name, |
| 369 | + name: idPrefix + this.desc.name |
| 370 | + } ); |
| 371 | + |
| 372 | + var value = options.values && options.values[this.desc.name]; |
| 373 | + if ( typeof value != 'undefined' ) { |
| 374 | + if ( value !== null && typeof value != 'number' ) { |
| 375 | + $.error( "value is invalid" ); |
| 376 | + } |
| 377 | + |
| 378 | + this.$text.val( value ); |
251 | 379 | } |
252 | 380 | |
253 | | - this.$text = $( '<input/>' ) |
254 | | - .attr( 'type', 'text' ) |
255 | | - .attr( 'id', idPrefix + this.desc.name ) |
256 | | - .attr( 'name', idPrefix + this.desc.name ) |
257 | | - .val( value ); |
258 | | - |
259 | | - this.$p.append( this.$text ); |
| 381 | + this.$div.append( this.$text ); |
260 | 382 | } |
261 | 383 | |
262 | 384 | NumberField.prototype.getValues = function() { |
263 | | - var val = parseFloat( this.$text.val() ), |
264 | | - res = {}; |
265 | | - res[this.desc.name] = isNaN( val ) ? null : val; |
266 | | - return res; |
| 385 | + var val = parseFloat( this.$text.val() ); |
| 386 | + return pair( this.desc.name, isNaN( val ) ? null : val ); |
267 | 387 | }; |
268 | 388 | |
269 | 389 | NumberField.prototype.getValidationSettings = function() { |
270 | | - var settings = LabelField.prototype.getValidationSettings.call( this ), |
| 390 | + var settings = SimpleField.prototype.getValidationSettings.call( this ), |
271 | 391 | fieldId = idPrefix + this.desc.name; |
272 | 392 | |
273 | 393 | settings.rules[fieldId] = {}; |
— | — | @@ -300,137 +420,137 @@ |
301 | 421 | return settings; |
302 | 422 | }; |
303 | 423 | |
| 424 | + validFieldTypes["number"] = NumberField; |
| 425 | + |
304 | 426 | /* A field with a drop-down list */ |
305 | | - SelectField.prototype = object( LabelField.prototype ); |
| 427 | + SelectField.prototype = object( SimpleField.prototype ); |
306 | 428 | SelectField.prototype.constructor = SelectField; |
307 | | - function SelectField( $form, desc, values ){ |
308 | | - LabelField.call( this, $form, desc, values ); |
| 429 | + function SelectField( desc, options ){ |
| 430 | + SimpleField.call( this, desc, options ); |
309 | 431 | |
310 | | - var $select = this.$select = $( '<select/>' ) |
311 | | - .attr( 'id', idPrefix + this.desc.name ) |
312 | | - .attr( 'name', idPrefix + this.desc.name ); |
| 432 | + var $select = this.$select = $( '<select/>' ).attr( { |
| 433 | + id: idPrefix + this.desc.name, |
| 434 | + name: idPrefix + this.desc.name |
| 435 | + } ); |
313 | 436 | |
314 | 437 | var validValues = []; |
315 | 438 | var self = this; |
316 | | - $.each( desc.options, function( optName, optVal ) { |
| 439 | + $.each( this.desc.options, function( idx, option ) { |
317 | 440 | var i = validValues.length; |
318 | 441 | $( '<option/>' ) |
319 | | - .text( preproc( self.$form, optName ) ) |
| 442 | + .text( preproc( self.prefix, option.name ) ) |
320 | 443 | .val( i ) |
321 | 444 | .appendTo( $select ); |
322 | | - validValues.push( optVal ); |
| 445 | + validValues.push( option.value ); |
323 | 446 | } ); |
324 | 447 | |
325 | 448 | this.validValues = validValues; |
326 | 449 | |
327 | | - var value = values[this.desc.name]; |
328 | | - if ( $.inArray( value, validValues ) == -1 ) { |
329 | | - $.error( "value is not in the list of possible values" ); |
| 450 | + var value = options.values && options.values[this.desc.name]; |
| 451 | + if ( typeof value != 'undefined' ) { |
| 452 | + if ( $.inArray( value, validValues ) == -1 ) { |
| 453 | + $.error( "value is not in the list of possible values" ); |
| 454 | + } |
| 455 | + |
| 456 | + var i = $.inArray( value, validValues ); |
| 457 | + $select.val( i ).prop( 'selected', 'selected' ); |
330 | 458 | } |
331 | 459 | |
332 | | - var i = $.inArray( value, validValues ); |
333 | | - $select.val( i ).attr( 'selected', 'selected' ); |
334 | | - |
335 | | - this.$p.append( $select ); |
| 460 | + this.$div.append( $select ); |
336 | 461 | } |
337 | 462 | |
338 | 463 | SelectField.prototype.getValues = function() { |
339 | | - var i = parseInt( this.$select.val(), 10 ), |
340 | | - res = {}; |
341 | | - res[this.desc.name] = this.validValues[i]; |
342 | | - return res; |
| 464 | + var i = parseInt( this.$select.val(), 10 ); |
| 465 | + return pair( this.desc.name, this.validValues[i] ); |
343 | 466 | }; |
344 | 467 | |
| 468 | + validFieldTypes["select"] = SelectField; |
345 | 469 | |
346 | 470 | /* A field with a slider, representing ranges of numbers */ |
347 | | - RangeField.prototype = object( LabelField.prototype ); |
| 471 | + RangeField.prototype = object( SimpleField.prototype ); |
348 | 472 | RangeField.prototype.constructor = RangeField; |
349 | | - function RangeField( $form, desc, values ){ |
350 | | - LabelField.call( this, $form, desc, values ); |
| 473 | + function RangeField( desc, options ){ |
| 474 | + SimpleField.call( this, desc, options ); |
351 | 475 | |
352 | | - var value = values[this.desc.name]; |
353 | | - if ( typeof value != 'number' ) { |
354 | | - $.error( "value is invalid" ); |
355 | | - } |
356 | | - |
357 | | - if ( typeof desc.min != 'number' ) { |
| 476 | + if ( typeof this.desc.min != 'number' ) { |
358 | 477 | $.error( "desc.min is invalid" ); |
359 | 478 | } |
360 | 479 | |
361 | | - if ( typeof desc.max != 'number' ) { |
| 480 | + if ( typeof this.desc.max != 'number' ) { |
362 | 481 | $.error( "desc.max is invalid" ); |
363 | 482 | } |
364 | 483 | |
365 | | - if ( typeof desc.step != 'undefined' && typeof desc.step != 'number' ) { |
| 484 | + if ( typeof this.desc.step != 'undefined' && typeof this.desc.step != 'number' ) { |
366 | 485 | $.error( "desc.step is invalid" ); |
367 | 486 | } |
368 | 487 | |
369 | | - if ( value < desc.min || value > desc.max ) { |
370 | | - $.error( "value is out of range" ); |
| 488 | + var value = options.values && options.values[this.desc.name]; |
| 489 | + if ( typeof value != 'undefined' ) { |
| 490 | + if ( typeof value != 'number' ) { |
| 491 | + $.error( "value is invalid" ); |
| 492 | + } |
| 493 | + if ( value < this.desc.min || value > this.desc.max ) { |
| 494 | + $.error( "value is out of range" ); |
| 495 | + } |
371 | 496 | } |
372 | 497 | |
373 | 498 | var $slider = this.$slider = $( '<div/>' ) |
374 | 499 | .attr( 'id', idPrefix + this.desc.name ); |
375 | 500 | |
376 | | - var options = { |
377 | | - min: desc.min, |
378 | | - max: desc.max, |
379 | | - value: value |
| 501 | + var rangeOptions = { |
| 502 | + min: this.desc.min, |
| 503 | + max: this.desc.max |
380 | 504 | }; |
381 | 505 | |
382 | | - if ( typeof desc.step != 'undefined' ) { |
383 | | - options['step'] = desc.step; |
| 506 | + if ( typeof value != 'undefined' ) { |
| 507 | + rangeOptions['value'] = value; |
384 | 508 | } |
385 | 509 | |
386 | | - $slider.slider( options ); |
| 510 | + if ( typeof this.desc.step != 'undefined' ) { |
| 511 | + rangeOptions['step'] = this.desc.step; |
| 512 | + } |
387 | 513 | |
388 | | - this.$p.append( $slider ); |
| 514 | + $slider.slider( rangeOptions ); |
| 515 | + |
| 516 | + this.$div.append( $slider ); |
389 | 517 | } |
390 | 518 | |
391 | 519 | RangeField.prototype.getValues = function() { |
392 | | - var res = {}; |
393 | | - res[this.desc.name] = this.$slider.slider( 'value' ); |
394 | | - return res; |
| 520 | + return pair( this.desc.name, this.$slider.slider( 'value' ) ); |
395 | 521 | }; |
| 522 | + |
| 523 | + validFieldTypes["range"] = RangeField; |
396 | 524 | |
397 | | - |
398 | 525 | /* A field with a textbox with a datepicker */ |
399 | | - DateField.prototype = object( LabelField.prototype ); |
| 526 | + DateField.prototype = object( SimpleField.prototype ); |
400 | 527 | DateField.prototype.constructor = DateField; |
401 | | - function DateField( $form, desc, values ){ |
402 | | - LabelField.call( this, $form, desc, values ); |
| 528 | + function DateField( desc, options ){ |
| 529 | + SimpleField.call( this, desc, options ); |
403 | 530 | |
404 | | - var value = values[this.desc.name]; |
405 | | - if ( typeof value == 'undefined' ) { |
406 | | - $.error( "value is invalid" ); |
407 | | - } |
| 531 | + this.$text = $( '<input/>' ).attr( { |
| 532 | + type: 'text', |
| 533 | + id: idPrefix + this.desc.name, |
| 534 | + name: idPrefix + this.desc.name |
| 535 | + } ).datepicker( { |
| 536 | + onSelect: function() { |
| 537 | + //Force validation, so that a previous 'invalid' state is removed |
| 538 | + $( this ).valid(); |
| 539 | + } |
| 540 | + } ); |
408 | 541 | |
| 542 | + var value = options.values && options.values[this.desc.name]; |
409 | 543 | var date; |
410 | | - if ( value !== null ) { |
| 544 | + if ( typeof value != 'undefined' && value !== null ) { |
411 | 545 | date = new Date( value ); |
412 | 546 | |
413 | 547 | if ( !isFinite( date ) ) { |
414 | 548 | $.error( "value is invalid" ); |
415 | 549 | } |
416 | | - } |
417 | 550 | |
418 | | - this.$text = $( '<input/>' ) |
419 | | - .attr( 'type', 'text' ) |
420 | | - .attr( 'id', idPrefix + this.desc.name ) |
421 | | - .attr( 'name', idPrefix + this.desc.name ) |
422 | | - .datepicker( { |
423 | | - onSelect: function() { |
424 | | - //Force validation, so that a previous 'invalid' state is removed |
425 | | - $( this ).valid(); |
426 | | - } |
427 | | - } ); |
428 | | - |
429 | | - if ( value !== null ) { |
430 | 551 | this.$text.datepicker( 'setDate', date ); |
431 | 552 | } |
432 | 553 | |
433 | | - |
434 | | - this.$p.append( this.$text ); |
| 554 | + this.$div.append( this.$text ); |
435 | 555 | } |
436 | 556 | |
437 | 557 | DateField.prototype.getValues = function() { |
— | — | @@ -442,18 +562,17 @@ |
443 | 563 | } |
444 | 564 | |
445 | 565 | //UTC date in ISO 8601 format [YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]Z |
446 | | - res[this.desc.name] = '' + |
| 566 | + return pair( this.desc.name, '' + |
447 | 567 | pad( d.getUTCFullYear(), 4 ) + '-' + |
448 | 568 | pad( d.getUTCMonth() + 1, 2 ) + '-' + |
449 | 569 | pad( d.getUTCDate(), 2 ) + 'T' + |
450 | 570 | pad( d.getUTCHours(), 2 ) + ':' + |
451 | 571 | pad( d.getUTCMinutes(), 2 ) + ':' + |
452 | | - pad( d.getUTCSeconds(), 2 ) + 'Z'; |
453 | | - return res; |
| 572 | + pad( d.getUTCSeconds(), 2 ) + 'Z' ); |
454 | 573 | }; |
455 | 574 | |
456 | 575 | DateField.prototype.getValidationSettings = function() { |
457 | | - var settings = LabelField.prototype.getValidationSettings.call( this ), |
| 576 | + var settings = SimpleField.prototype.getValidationSettings.call( this ), |
458 | 577 | fieldId = idPrefix + this.desc.name; |
459 | 578 | |
460 | 579 | settings.rules[fieldId] = { |
— | — | @@ -462,6 +581,8 @@ |
463 | 582 | return settings; |
464 | 583 | }; |
465 | 584 | |
| 585 | + validFieldTypes["date"] = DateField; |
| 586 | + |
466 | 587 | /* A field with color picker */ |
467 | 588 | |
468 | 589 | function closeColorPicker() { |
— | — | @@ -470,7 +591,6 @@ |
471 | 592 | } ); |
472 | 593 | } |
473 | 594 | |
474 | | - |
475 | 595 | //If a click happens outside the colorpicker while it is showed, remove it |
476 | 596 | $( document ).mousedown( function( event ) { |
477 | 597 | var $target = $( event.target ); |
— | — | @@ -479,25 +599,24 @@ |
480 | 600 | } |
481 | 601 | } ); |
482 | 602 | |
483 | | - ColorField.prototype = object( LabelField.prototype ); |
| 603 | + ColorField.prototype = object( SimpleField.prototype ); |
484 | 604 | ColorField.prototype.constructor = ColorField; |
485 | | - function ColorField( $form, desc, values ){ |
486 | | - LabelField.call( this, $form, desc, values ); |
| 605 | + function ColorField( desc, options ){ |
| 606 | + SimpleField.call( this, desc, options ); |
487 | 607 | |
488 | | - var value = values[this.desc.name]; |
489 | | - if ( typeof value == 'undefined' ) { |
490 | | - $.error( "value is invalid" ); |
491 | | - } |
| 608 | + var value = ( options.values && options.values[this.desc.name] ) || ''; |
492 | 609 | |
493 | | - this.$text = $( '<input/>' ) |
494 | | - .attr( 'type', 'text' ) |
495 | | - .attr( 'id', idPrefix + this.desc.name ) |
496 | | - .attr( 'name', idPrefix + this.desc.name ) |
| 610 | + this.$text = $( '<input/>' ).attr( { |
| 611 | + type: 'text', |
| 612 | + id: idPrefix + this.desc.name, |
| 613 | + name: idPrefix + this.desc.name |
| 614 | + } ) |
497 | 615 | .addClass( 'colorpicker-input' ) |
498 | 616 | .val( value ) |
499 | 617 | .css( 'background-color', value ) |
500 | 618 | .focus( function() { |
501 | 619 | $( '<div/>' ) |
| 620 | + .addClass( 'ui-widget ui-widget-content' ) |
502 | 621 | .attr( 'id', 'colorpicker' ) |
503 | 622 | .css( 'position', 'absolute' ) |
504 | 623 | .hide() |
— | — | @@ -525,11 +644,11 @@ |
526 | 645 | } ) |
527 | 646 | .blur( closeColorPicker ); |
528 | 647 | |
529 | | - this.$p.append( this.$text ); |
| 648 | + this.$div.append( this.$text ); |
530 | 649 | } |
531 | 650 | |
532 | 651 | ColorField.prototype.getValidationSettings = function() { |
533 | | - var settings = LabelField.prototype.getValidationSettings.call( this ), |
| 652 | + var settings = SimpleField.prototype.getValidationSettings.call( this ), |
534 | 653 | fieldId = idPrefix + this.desc.name; |
535 | 654 | |
536 | 655 | settings.rules[fieldId] = { |
— | — | @@ -539,128 +658,657 @@ |
540 | 659 | }; |
541 | 660 | |
542 | 661 | ColorField.prototype.getValues = function() { |
543 | | - var color = $.colorUtil.getRGB( this.$text.val() ), |
544 | | - res = {}; |
545 | | - res[this.desc.name] = '#' + pad( color[0].toString( 16 ), 2 ) + |
546 | | - pad( color[1].toString( 16 ), 2 ) + pad( color[2].toString( 16 ), 2 ); |
547 | | - return res; |
| 662 | + var color = $.colorUtil.getRGB( this.$text.val() ); |
| 663 | + return pair( this.desc.name, '#' + pad( color[0].toString( 16 ), 2 ) + |
| 664 | + pad( color[1].toString( 16 ), 2 ) + pad( color[2].toString( 16 ), 2 ) ); |
548 | 665 | }; |
| 666 | + |
| 667 | + validFieldTypes["color"] = ColorField; |
549 | 668 | |
| 669 | + |
550 | 670 | /* A field that represent a section (group of fields) */ |
| 671 | + |
| 672 | + function deleteFieldRules( field ) { |
| 673 | + //Remove all its validation rules |
| 674 | + var validationSettings = field.getValidationSettings(); |
| 675 | + if ( validationSettings.rules ) { |
| 676 | + $.each( validationSettings.rules, function( name, value ) { |
| 677 | + var $input = $( '#' + name ); |
| 678 | + if ( $input.length > 0 ) { |
| 679 | + $( '#' + name ).rules( 'remove' ); |
| 680 | + } |
| 681 | + } ); |
| 682 | + } |
| 683 | + } |
| 684 | + |
| 685 | + function addFieldRules( field ) { |
| 686 | + var validationSettings = field.getValidationSettings(); |
| 687 | + if ( validationSettings.rules ) { |
| 688 | + $.each( validationSettings.rules, function( name, rules ) { |
| 689 | + var $input = $( '#' + name ); |
| 690 | + |
| 691 | + //Find messages associated to this rule, if any |
| 692 | + if ( typeof validationSettings.messages != 'undefined' && |
| 693 | + typeof validationSettings.messages[name] != 'undefined') |
| 694 | + { |
| 695 | + rules.messages = validationSettings.messages[name]; |
| 696 | + } |
| 697 | + |
| 698 | + if ( $input.length > 0 ) { |
| 699 | + $( '#' + name ).rules( 'add', rules ); |
| 700 | + } |
| 701 | + } ); |
| 702 | + } |
| 703 | + } |
| 704 | + |
| 705 | + |
551 | 706 | SectionField.prototype = object( Field.prototype ); |
552 | 707 | SectionField.prototype.constructor = SectionField; |
553 | | - function SectionField( $form, desc, values, id ) { |
554 | | - Field.call( this, $form, desc, values ); |
| 708 | + function SectionField( desc, options, id ) { |
| 709 | + Field.call( this, desc, options ); |
555 | 710 | |
556 | | - this.$p = $( '<p/>' ); |
| 711 | + this.$div = $( '<div/>' ).data( 'field', this ); |
557 | 712 | |
558 | 713 | if ( id !== undefined ) { |
559 | | - this.$p.attr( 'id', id ); |
| 714 | + this.$div.attr( 'id', id ); |
560 | 715 | } |
561 | | - |
562 | | - var fields = [], |
563 | | - settings = {}; //validator settings |
564 | 716 | |
565 | | - for ( var i = 0; i < desc.fields.length; i++ ) { |
| 717 | + //If there is an "intro", adds it to the section as a label |
| 718 | + //TODO: kill "intro"s and make "label" fields, instead? |
| 719 | + if ( typeof this.desc.intro == 'string' ) { |
| 720 | + $( '<p/>' ) |
| 721 | + .text( preproc( this.prefix, this.desc.intro ) ) |
| 722 | + .addClass( 'formBuilder-intro' ) |
| 723 | + .appendTo( this.$div ); |
| 724 | + } |
| 725 | + |
| 726 | + for ( var i = 0; i < this.desc.fields.length; i++ ) { |
566 | 727 | //TODO: validate fieldName |
567 | | - var field = desc.fields[i], |
| 728 | + |
| 729 | + if ( options.editable === true ) { |
| 730 | + //add an empty slot |
| 731 | + this._createSlot( true ).appendTo( this.$div ); |
| 732 | + } |
| 733 | + |
| 734 | + var field = this.desc.fields[i], |
568 | 735 | FieldConstructor = validFieldTypes[field.type]; |
569 | 736 | |
570 | 737 | if ( typeof FieldConstructor != 'function' ) { |
571 | 738 | $.error( "field with invalid type: " + field.type ); |
572 | 739 | } |
573 | 740 | |
574 | | - var f = new FieldConstructor( $form, field, values ); |
| 741 | + var f = new FieldConstructor( field, options ), |
| 742 | + $slot = this._createSlot( options.editable === true, f ); |
575 | 743 | |
576 | | - this.$p.append( f.getElement() ); |
577 | | - |
578 | | - //If this field has validation rules, add them to settings |
579 | | - var fieldSettings = f.getValidationSettings(); |
580 | | - |
581 | | - if ( fieldSettings ) { |
582 | | - $.extend( true, settings, fieldSettings ); |
583 | | - } |
584 | | - |
585 | | - fields.push( f ); |
| 744 | + $slot.appendTo( this.$div ); |
586 | 745 | } |
587 | 746 | |
588 | | - this.settings = settings; |
589 | | - this.fields = fields; |
| 747 | + if ( options.editable === true ) { |
| 748 | + //add an empty slot |
| 749 | + this._createSlot( true ).appendTo( this.$div ); |
| 750 | + } |
590 | 751 | } |
591 | 752 | |
592 | 753 | SectionField.prototype.getElement = function() { |
593 | | - return this.$p; |
| 754 | + return this.$div; |
594 | 755 | }; |
595 | 756 | |
| 757 | + SectionField.prototype.getDesc = function( useValuesAsDefaults ) { |
| 758 | + var desc = this.desc; |
| 759 | + desc.fields = []; |
| 760 | + this.$div.children().each( function( idx, slot ) { |
| 761 | + var field = $( slot ).data( 'field' ); |
| 762 | + if ( field !== undefined ) { |
| 763 | + desc.fields.push( field.getDesc( useValuesAsDefaults ) ); |
| 764 | + } |
| 765 | + } ); |
| 766 | + return desc; |
| 767 | + }; |
| 768 | + |
| 769 | + SectionField.prototype.setTitle = function( newTitle ) { |
| 770 | + this.desc.title = newTitle; |
| 771 | + }; |
| 772 | + |
596 | 773 | SectionField.prototype.getValues = function() { |
597 | 774 | var values = {}; |
598 | | - for ( var i = 0; i < this.fields.length; i++ ) { |
599 | | - $.extend( values, this.fields[i].getValues() ); |
600 | | - } |
| 775 | + this.$div.children().each( function( idx, slot ) { |
| 776 | + var field = $( slot ).data( 'field' ); |
| 777 | + if ( field !== undefined ) { |
| 778 | + $.extend( values, field.getValues() ); |
| 779 | + } |
| 780 | + } ); |
601 | 781 | return values; |
602 | 782 | }; |
603 | 783 | |
604 | 784 | SectionField.prototype.getValidationSettings = function() { |
605 | | - return this.settings; |
| 785 | + var settings = {}; |
| 786 | + this.$div.children().each( function( idx, slot ) { |
| 787 | + var field = $( slot ).data( 'field' ); |
| 788 | + if ( field !== undefined ) { |
| 789 | + var fieldSettings = $( slot ).data( 'field' ).getValidationSettings(); |
| 790 | + if ( fieldSettings ) { |
| 791 | + $.extend( true, settings, fieldSettings ); |
| 792 | + } |
| 793 | + } |
| 794 | + } ); |
| 795 | + |
| 796 | + return settings; |
606 | 797 | }; |
| 798 | + |
| 799 | + SectionField.prototype._createFieldDialog = function( params ) { |
| 800 | + var self = this; |
| 801 | + |
| 802 | + if ( typeof params.callback != 'function' ) { |
| 803 | + $.error( 'createFieldDialog: missing or wrong "callback" parameter' ); |
| 804 | + } |
| 805 | + |
| 806 | + var type, description, values; |
| 807 | + if ( typeof params.description == 'undefined' && typeof params.type == 'undefined' ) { |
| 808 | + //Create a dialog to choose the type of field to create |
| 809 | + var selectOptions = []; |
| 810 | + $.each( validFieldTypes, function( fieldType ) { |
| 811 | + selectOptions.push( { |
| 812 | + name: fieldType, |
| 813 | + value: fieldType |
| 814 | + } ); |
| 815 | + } ); |
| 816 | + |
| 817 | + $( { |
| 818 | + fields: [ { |
| 819 | + 'name': "type", |
| 820 | + 'type': "select", |
| 821 | + 'label': mw.msg( 'gadgets-formbuilder-editor-chose-field' ), |
| 822 | + 'options': selectOptions, |
| 823 | + 'default': selectOptions[0].value |
| 824 | + } ] |
| 825 | + } ).formBuilder( {} ).dialog( { |
| 826 | + width: 450, |
| 827 | + modal: true, |
| 828 | + resizable: false, |
| 829 | + title: mw.msg( 'gadgets-formbuilder-editor-chose-field-title' ), |
| 830 | + close: function() { |
| 831 | + $( this ).remove(); |
| 832 | + }, |
| 833 | + buttons: [ |
| 834 | + { |
| 835 | + text: mw.msg( 'gadgets-formbuilder-editor-ok' ), |
| 836 | + click: function() { |
| 837 | + var values = $( this ).formBuilder( 'getValues' ); |
| 838 | + $( this ).dialog( "close" ); |
| 839 | + self._createFieldDialog( { |
| 840 | + type: values.type, |
| 841 | + callback: params.callback |
| 842 | + } ); |
| 843 | + } |
| 844 | + }, |
| 845 | + { |
| 846 | + text: mw.msg( 'gadgets-formbuilder-editor-cancel' ), |
| 847 | + click: function() { |
| 848 | + $( this ).dialog( "close" ); |
| 849 | + } |
| 850 | + } |
| 851 | + ] |
| 852 | + } ); |
| 853 | + |
| 854 | + return; |
| 855 | + } else { |
| 856 | + type = params.type; |
| 857 | + if ( typeof prefsDescriptionSpecifications[type] == 'undefined' ) { |
| 858 | + $.error( 'createFieldDialog: invalid type: ' + type ); |
| 859 | + } |
| 860 | + |
| 861 | + description = { |
| 862 | + fields: prefsDescriptionSpecifications[type] |
| 863 | + }; |
| 864 | + } |
| 865 | + |
| 866 | + if ( typeof params.values != 'undefined' ) { |
| 867 | + values = params.values; |
| 868 | + } else { |
| 869 | + values = {}; |
| 870 | + //Set defaults (if given) as starting values |
| 871 | + //TODO: should field constructors just use default if no value is given? |
| 872 | + $.each( description.fields, function( idx, field ) { |
| 873 | + if ( typeof field['default'] != 'undefined' ) { |
| 874 | + values[field.name] = field['default']; |
| 875 | + } |
| 876 | + } ); |
| 877 | + } |
| 878 | + |
| 879 | + //Create the dialog to set field properties |
| 880 | + var dlg = $( '<div/>' ); |
| 881 | + var form = $( description ).formBuilder( { |
| 882 | + values: values |
| 883 | + } ).appendTo( dlg ); |
| 884 | + |
| 885 | + dlg.dialog( { |
| 886 | + modal: true, |
| 887 | + width: 550, |
| 888 | + resizable: false, |
| 889 | + title: mw.msg( 'gadgets-formbuilder-editor-create-field-title' ), |
| 890 | + close: function() { |
| 891 | + $( this ).remove(); |
| 892 | + }, |
| 893 | + buttons: [ |
| 894 | + { |
| 895 | + text: mw.msg( 'gadgets-formbuilder-editor-ok' ), |
| 896 | + click: function() { |
| 897 | + var isValid = $( form ).formBuilder( 'validate' ); |
| 898 | + |
| 899 | + if ( isValid ) { |
| 900 | + var fieldDescription = $( form ).formBuilder( 'getValues' ); |
| 901 | + |
| 902 | + if ( typeof type != 'undefined' ) { |
| 903 | + //Remove properties the equal their default |
| 904 | + $.each( description.fields, function( index, fieldSpec ) { |
| 905 | + var property = fieldSpec.name; |
| 906 | + if ( fieldDescription[property] === fieldSpec['default'] ) { |
| 907 | + delete fieldDescription[property]; |
| 908 | + } |
| 909 | + } ); |
| 910 | + } |
| 911 | + |
| 912 | + //Try to create the field. In case of error, warn the user. |
| 913 | + fieldDescription.type = type; |
| 914 | + |
| 915 | + var FieldConstructor = validFieldTypes[type]; |
| 916 | + var field; |
| 917 | + |
| 918 | + try { |
| 919 | + field = new FieldConstructor( fieldDescription, self.options ); |
| 920 | + } catch ( err ) { |
| 921 | + alert( "Invalid field options: " + err ); //TODO: i18n |
| 922 | + return; |
| 923 | + } |
| 924 | + |
| 925 | + if ( params.callback( field ) === true ) { |
| 926 | + $( this ).dialog( "close" ); |
| 927 | + } |
| 928 | + } |
| 929 | + } |
| 930 | + }, |
| 931 | + { |
| 932 | + text: mw.msg( 'gadgets-formbuilder-editor-cancel' ), |
| 933 | + click: function() { |
| 934 | + $( this ).dialog( "close" ); |
| 935 | + params.callback( null ); |
| 936 | + } |
| 937 | + } |
| 938 | + ] |
| 939 | + } ); |
| 940 | + }; |
| 941 | + |
| 942 | + SectionField.prototype._deleteSlot = function( $slot ) { |
| 943 | + var field = $slot.data( 'field' ); |
| 944 | + if ( field !== undefined ) { |
| 945 | + //Slot with a field |
| 946 | + deleteFieldRules( field ); |
| 947 | + } |
| 948 | + |
| 949 | + //Delete it |
| 950 | + $slot.remove(); |
| 951 | + }; |
607 | 952 | |
| 953 | + SectionField.prototype._createSlot = function( editable, field ) { |
| 954 | + var self = this, |
| 955 | + $slot = $( '<div/>' ).addClass( 'formbuilder-slot ui-widget' ), |
| 956 | + $divButtons; |
| 957 | + |
| 958 | + if ( editable ) { |
| 959 | + $slot.addClass( 'formbuilder-slot-editable' ); |
| 960 | + |
| 961 | + $divButtons = $( '<div/>' ) |
| 962 | + .addClass( 'formbuilder-editor-slot-buttons' ) |
| 963 | + .appendTo( $slot ); |
| 964 | + } |
| 965 | + |
| 966 | + if ( typeof field != 'undefined' ) { |
| 967 | + //Nonempty slot |
| 968 | + $slot.prepend( field.getElement() ) |
| 969 | + .data( 'field', field ); |
| 970 | + |
| 971 | + if ( editable ) { |
| 972 | + $slot.addClass( 'formbuilder-slot-nonempty' ); |
| 973 | + |
| 974 | + //Add the handle for moving slots |
| 975 | + $( '<span />' ) |
| 976 | + .addClass( 'formbuilder-editor-button formbuilder-editor-button-move ui-icon ui-icon-arrow-4' ) |
| 977 | + .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-move' ) ) |
| 978 | + .mousedown( function() { |
| 979 | + $( this ).focus(); |
| 980 | + } ) |
| 981 | + .appendTo( $divButtons ); |
| 982 | + |
| 983 | + //Add the button for changing existing slots |
| 984 | + $( '<a href="javascript:;" />' ) |
| 985 | + .addClass( 'formbuilder-editor-button formbuilder-editor-button-edit ui-icon ui-icon-gear' ) |
| 986 | + .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-edit-field' ) ) |
| 987 | + .click( function() { |
| 988 | + self._createFieldDialog( { |
| 989 | + type: field.getDesc().type, |
| 990 | + values: field.getDesc(), |
| 991 | + callback: function( newField ) { |
| 992 | + if ( newField !== null ) { |
| 993 | + //check that there are no duplicate preference names |
| 994 | + var existingValues = self.$div.closest( 'form' ).formBuilder( 'getValues' ), |
| 995 | + removedValues = field.getValues(), |
| 996 | + duplicateName = null; |
| 997 | + $.each( field.getValues(), function( name, val ) { |
| 998 | + //Only complain for preference names that are not in names for the field being replaced |
| 999 | + if ( typeof existingValues[name] != 'undefined' && removedValues[name] == 'undefined' ) { |
| 1000 | + duplicateName = name; |
| 1001 | + return false; |
| 1002 | + } |
| 1003 | + } ); |
| 1004 | + |
| 1005 | + if ( duplicateName !== null ) { |
| 1006 | + alert( mw.msg( 'gadgets-formbuilder-editor-duplicate-name', duplicateName ) ); |
| 1007 | + return false; |
| 1008 | + } |
| 1009 | + |
| 1010 | + var $newSlot = self._createSlot( true, newField ); |
| 1011 | + |
| 1012 | + deleteFieldRules( field ); |
| 1013 | + |
| 1014 | + $slot.replaceWith( $newSlot ); |
| 1015 | + |
| 1016 | + //Add field's validation rules |
| 1017 | + addFieldRules( newField ); |
| 1018 | + } |
| 1019 | + return true; |
| 1020 | + } |
| 1021 | + } ); |
| 1022 | + } ) |
| 1023 | + .appendTo( $divButtons ); |
| 1024 | + |
| 1025 | + //Add the button to delete slots |
| 1026 | + $( '<a href="javascript:;" />' ) |
| 1027 | + .addClass( 'formbuilder-editor-button formbuilder-editor-button-delete ui-icon ui-icon-trash' ) |
| 1028 | + .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete-field' ) ) |
| 1029 | + .click( function( event, ui ) { |
| 1030 | + //Make both slots disappear, then delete them |
| 1031 | + $.each( [$slot, $slot.prev()], function( idx, $s ) { |
| 1032 | + $s.slideUp( function() { |
| 1033 | + self._deleteSlot( $s ); |
| 1034 | + } ); |
| 1035 | + } ); |
| 1036 | + } ) |
| 1037 | + .appendTo( $divButtons ); |
| 1038 | + |
| 1039 | + //Make this slot draggable to allow moving it |
| 1040 | + $slot.draggable( { |
| 1041 | + revert: true, |
| 1042 | + handle: ".formbuilder-editor-button-move", |
| 1043 | + helper: "original", |
| 1044 | + zIndex: $slot.closest( 'form' ).zIndex() + 1000, //TODO: ugly, find a better way |
| 1045 | + opacity: 0.8, |
| 1046 | + cursor: "move", |
| 1047 | + cursorAt: { |
| 1048 | + top: -5, |
| 1049 | + left: -5 |
| 1050 | + } |
| 1051 | + } ); |
| 1052 | + } |
| 1053 | + } else { |
| 1054 | + //Create empty slot |
| 1055 | + $slot.addClass( 'formbuilder-slot-empty' ) |
| 1056 | + .droppable( { |
| 1057 | + hoverClass: 'formbuilder-slot-can-drop', |
| 1058 | + tolerance: 'pointer', |
| 1059 | + drop: function( event, ui ) { |
| 1060 | + var srcSlot = ui.draggable, dstSlot = this; |
| 1061 | + |
| 1062 | + //Remove one empty slot surrounding source |
| 1063 | + $( srcSlot ).prev().remove(); |
| 1064 | + |
| 1065 | + //Replace dstSlot with srcSlot: |
| 1066 | + $( dstSlot ).replaceWith( srcSlot ); |
| 1067 | + |
| 1068 | + //Add one empty slot before and one after the new position |
| 1069 | + self._createSlot( true ).insertBefore( srcSlot ); |
| 1070 | + self._createSlot( true ).insertAfter( srcSlot ); |
| 1071 | + }, |
| 1072 | + accept: function( draggable ) { |
| 1073 | + //All non empty slots accepted, except for closest siblings |
| 1074 | + return $( draggable ).hasClass( 'formbuilder-slot-nonempty' ) && |
| 1075 | + $( draggable ).prev().get( 0 ) !== $slot.get( 0 ) && |
| 1076 | + $( draggable ).next().get( 0 ) !== $slot.get( 0 ); |
| 1077 | + } |
| 1078 | + } ); |
| 1079 | + |
| 1080 | + //The button to create a new field |
| 1081 | + $( '<a href="javascript:;" />' ) |
| 1082 | + .addClass( 'formbuilder-editor-button formbuilder-editor-button-new ui-icon ui-icon-plus' ) |
| 1083 | + .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-insert-field' ) ) |
| 1084 | + .click( function() { |
| 1085 | + self._createFieldDialog( { |
| 1086 | + callback: function( field ) { |
| 1087 | + if ( field !== null ) { |
| 1088 | + //check that there are no duplicate preference names |
| 1089 | + var existingValues = $slot.closest( 'form' ).formBuilder( 'getValues' ), |
| 1090 | + duplicateName = null; |
| 1091 | + $.each( field.getValues(), function( name, val ) { |
| 1092 | + if ( typeof existingValues[name] != 'undefined' ) { |
| 1093 | + duplicateName = name; |
| 1094 | + return false; |
| 1095 | + } |
| 1096 | + } ); |
| 1097 | + |
| 1098 | + if ( duplicateName !== null ) { |
| 1099 | + alert( mw.msg( 'gadgets-formbuilder-editor-duplicate-name' , duplicateName ) ); |
| 1100 | + return false; |
| 1101 | + } |
| 1102 | + |
| 1103 | + var $newSlot = self._createSlot( true, field ).hide(), |
| 1104 | + $newEmptySlot = self._createSlot( true ).hide(); |
| 1105 | + |
| 1106 | + $slot.after( $newSlot, $newEmptySlot ); |
| 1107 | + |
| 1108 | + $newSlot.slideDown(); |
| 1109 | + $newEmptySlot.slideDown(); |
| 1110 | + |
| 1111 | + //Add field's validation rules |
| 1112 | + addFieldRules( field ); |
| 1113 | + } |
| 1114 | + return true; |
| 1115 | + } |
| 1116 | + } ); |
| 1117 | + } ) |
| 1118 | + .appendTo( $divButtons ); |
| 1119 | + } |
| 1120 | + |
| 1121 | + return $slot; |
| 1122 | + }; |
| 1123 | + |
| 1124 | + |
608 | 1125 | /* A field for 'bundle's */ |
609 | 1126 | BundleField.prototype = object( EmptyField.prototype ); |
610 | 1127 | BundleField.prototype.constructor = BundleField; |
611 | | - function BundleField( $form, desc, values ) { |
612 | | - EmptyField.call( this, $form, desc, values ); |
| 1128 | + function BundleField( desc, options ) { |
| 1129 | + EmptyField.call( this, desc, options ); |
613 | 1130 | |
614 | 1131 | //Create tabs |
615 | 1132 | var $tabs = this.$tabs = $( '<div><ul></ul></div>' ) |
616 | 1133 | .attr( 'id', idPrefix + 'tab-' + getIncrementalCounter() ) |
617 | | - .tabs(); |
| 1134 | + .tabs( { |
| 1135 | + add: function( event, ui ) { |
| 1136 | + //Links the anchor to the panel |
| 1137 | + $( ui.tab ).data( 'panel', ui.panel ); |
| 1138 | + |
| 1139 | + if ( options.editable === true ) { |
| 1140 | + //Add "delete section" button |
| 1141 | + $( '<span />' ) |
| 1142 | + .addClass( 'formbuilder-editor-button formbuilder-editor-button-delete-section ui-icon ui-icon-trash' ) |
| 1143 | + .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete-section' ) ) |
| 1144 | + .click( function() { |
| 1145 | + var sectionField = $( ui.panel ).data( 'field' ); |
| 1146 | + deleteFieldRules( sectionField ); |
618 | 1147 | |
619 | | - this.sections = []; |
| 1148 | + var index = $( "li", $tabs ).index( $( this ).closest( "li" ) ); |
| 1149 | + index -= 1; //Don't count the "add section" button |
| 1150 | + |
| 1151 | + $tabs.tabs( 'remove', index ); |
| 1152 | + } ) |
| 1153 | + .appendTo( ui.tab ); |
620 | 1154 | |
| 1155 | + //Add "edit section" button |
| 1156 | + $( '<span />' ) |
| 1157 | + .addClass( 'formbuilder-editor-button formbuilder-editor-button-edit-section ui-icon ui-icon-gear' ) |
| 1158 | + .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-edit-section' ) ) |
| 1159 | + .click( function() { |
| 1160 | + var button = this, |
| 1161 | + sectionField = $( ui.panel ).data( 'field' ); |
| 1162 | + |
| 1163 | + $( { |
| 1164 | + fields: [ { |
| 1165 | + 'name': "title", |
| 1166 | + 'type': "string", |
| 1167 | + 'label': mw.msg( 'gadgets-formbuilder-editor-chose-title' ) |
| 1168 | + } ] |
| 1169 | + } ).formBuilder( { |
| 1170 | + values: { |
| 1171 | + title: sectionField.getDesc().title |
| 1172 | + } |
| 1173 | + } ).dialog( { |
| 1174 | + modal: true, |
| 1175 | + resizable: false, |
| 1176 | + title: mw.msg( 'gadgets-formbuilder-editor-chose-title-title' ), |
| 1177 | + close: function() { |
| 1178 | + $( this ).remove(); //completely destroy on close |
| 1179 | + }, |
| 1180 | + buttons: [ |
| 1181 | + { |
| 1182 | + text: mw.msg( 'gadgets-formbuilder-editor-ok' ), |
| 1183 | + click: function() { |
| 1184 | + var title = $( this ).formBuilder( 'getValues' ).title; |
| 1185 | + |
| 1186 | + //Update field description |
| 1187 | + sectionField.setTitle( title ); |
| 1188 | + |
| 1189 | + //Update tab's title |
| 1190 | + $( button ).parent().find( ':first-child' ).text( title ); |
| 1191 | + |
| 1192 | + $( this ).dialog( "close" ); |
| 1193 | + } |
| 1194 | + }, |
| 1195 | + { |
| 1196 | + text: mw.msg( 'gadgets-formbuilder-editor-cancel' ), |
| 1197 | + click: function() { |
| 1198 | + $( this ).dialog( "close" ); |
| 1199 | + } |
| 1200 | + } |
| 1201 | + ] |
| 1202 | + } ); |
| 1203 | + } ) |
| 1204 | + .appendTo( ui.tab ); |
| 1205 | + } |
| 1206 | + } |
| 1207 | + } ); |
| 1208 | + |
621 | 1209 | var self = this; |
622 | | - $.each( desc.sections, function( sectionName, sectionDescription ) { |
| 1210 | + $.each( this.desc.sections, function( index, sectionDescription ) { |
623 | 1211 | var id = idPrefix + 'section-' + getIncrementalCounter(), |
624 | | - sec = new SectionField( $form, sectionDescription, values, id ); |
| 1212 | + sec = new SectionField( sectionDescription, options, id ); |
625 | 1213 | |
626 | | - self.sections.push( sec ); |
627 | | - |
628 | 1214 | $tabs.append( sec.getElement() ) |
629 | | - .tabs( 'add', '#' + id, preproc( $form, sectionName ) ); |
| 1215 | + .tabs( 'add', '#' + id, preproc( options.prefix, sectionDescription.title ) ); |
630 | 1216 | } ); |
631 | 1217 | |
632 | | - this.$p.append( $tabs ); |
| 1218 | + if ( options.editable === true ) { |
| 1219 | + //Add the button to create a new section |
| 1220 | + $( '<span>' ) |
| 1221 | + .addClass( 'formbuilder-editor-button formbuilder-editor-button-new-section ui-icon ui-icon-plus' ) |
| 1222 | + .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-new-section' ) ) |
| 1223 | + .click( function() { |
| 1224 | + $( { |
| 1225 | + fields: [ { |
| 1226 | + 'name': "title", |
| 1227 | + 'type': "string", |
| 1228 | + 'label': mw.msg( 'gadgets-formbuilder-editor-chose-title' ) |
| 1229 | + } ] |
| 1230 | + } ).formBuilder( {} ).dialog( { |
| 1231 | + modal: true, |
| 1232 | + resizable: false, |
| 1233 | + title: mw.msg( 'gadgets-formbuilder-editor-chose-title-title' ), |
| 1234 | + close: function() { |
| 1235 | + $( this ).remove(); |
| 1236 | + }, |
| 1237 | + buttons: [ |
| 1238 | + { |
| 1239 | + text: mw.msg( 'gadgets-formbuilder-editor-ok' ), |
| 1240 | + click: function() { |
| 1241 | + var title = $( this ).formBuilder( 'getValues' ).title, |
| 1242 | + id = idPrefix + 'section-' + getIncrementalCounter(), |
| 1243 | + newSectionDescription = { |
| 1244 | + title: title, |
| 1245 | + fields: [] |
| 1246 | + }, |
| 1247 | + newSection = new SectionField( newSectionDescription, options, id ); |
| 1248 | + |
| 1249 | + $tabs.append( newSection.getElement() ) |
| 1250 | + .tabs( 'add', '#' + id, preproc( options.prefix, title ) ); |
| 1251 | + |
| 1252 | + $( this ).dialog( "close" ); |
| 1253 | + } |
| 1254 | + }, |
| 1255 | + { |
| 1256 | + text: mw.msg( 'gadgets-formbuilder-editor-cancel' ), |
| 1257 | + click: function() { |
| 1258 | + $( this ).dialog( "close" ); |
| 1259 | + } |
| 1260 | + } |
| 1261 | + ] |
| 1262 | + } ); |
| 1263 | + } ) |
| 1264 | + .wrap( '<li />' ).parent() |
| 1265 | + .prependTo( $tabs.find('.ui-tabs-nav' ) ); |
| 1266 | + |
| 1267 | + //Make the tabs sortable |
| 1268 | + $tabs.tabs().find( '.ui-tabs-nav' ).sortable( { |
| 1269 | + axis: 'x', |
| 1270 | + items: 'li:not(:has(.formbuilder-editor-button-new-section))' |
| 1271 | + } ); |
| 1272 | + } |
| 1273 | + |
| 1274 | + this.$div.append( $tabs ); |
633 | 1275 | } |
634 | 1276 | |
635 | 1277 | BundleField.prototype.getValidationSettings = function() { |
636 | 1278 | var settings = {}; |
637 | | - $.each( this.sections, function( idx, section ) { |
638 | | - $.extend( true, settings, section.getValidationSettings() ); |
| 1279 | + this.$div.find( '.ui-tabs-nav a' ).each( function( idx, anchor ) { |
| 1280 | + var panel = $( anchor ).data( 'panel' ), |
| 1281 | + field = $( panel ).data( 'field' ); |
| 1282 | + |
| 1283 | + $.extend( true, settings, field.getValidationSettings() ); |
639 | 1284 | } ); |
640 | 1285 | return settings; |
641 | 1286 | }; |
642 | 1287 | |
| 1288 | + BundleField.prototype.getDesc = function( useValuesAsDefaults ) { |
| 1289 | + var desc = this.desc; |
| 1290 | + desc.sections = []; |
| 1291 | + this.$div.find( '.ui-tabs-nav a' ).each( function( idx, anchor ) { |
| 1292 | + var panel = $( anchor ).data( 'panel' ), |
| 1293 | + field = $( panel ).data( 'field' ); |
| 1294 | + |
| 1295 | + desc.sections.push( field.getDesc( useValuesAsDefaults ) ); |
| 1296 | + } ); |
| 1297 | + return desc; |
| 1298 | + }; |
| 1299 | + |
643 | 1300 | BundleField.prototype.getValues = function() { |
644 | 1301 | var values = {}; |
645 | | - $.each( this.sections, function( idx, section ) { |
646 | | - $.extend( values, section.getValues() ); |
| 1302 | + this.$div.find( '.ui-tabs-nav a' ).each( function( idx, anchor ) { |
| 1303 | + var panel = $( anchor ).data( 'panel' ), |
| 1304 | + field = $( panel ).data( 'field' ); |
| 1305 | + |
| 1306 | + $.extend( values, field.getValues() ); |
647 | 1307 | } ); |
648 | 1308 | return values; |
649 | 1309 | }; |
650 | 1310 | |
| 1311 | + validFieldTypes["bundle"] = BundleField; |
651 | 1312 | |
652 | | - //Field types that can be referred to by preference descriptions |
653 | | - var validFieldTypes = { |
654 | | - "boolean": BooleanField, |
655 | | - "string" : StringField, |
656 | | - "number" : NumberField, |
657 | | - "select" : SelectField, |
658 | | - "range" : RangeField, |
659 | | - "date" : DateField, |
660 | | - "color" : ColorField, |
661 | | - "bundle" : BundleField |
662 | | - }; |
663 | | - |
664 | | - |
665 | 1313 | /* Public methods */ |
666 | 1314 | |
667 | 1315 | /** |
— | — | @@ -670,8 +1318,8 @@ |
671 | 1319 | * @param {Object} options |
672 | 1320 | * @return {Element} the object with the requested form body. |
673 | 1321 | */ |
674 | | - function buildFormBody( options ) { |
675 | | - var description = this.get( 0 ); |
| 1322 | + function buildFormBody( options ) { |
| 1323 | + var description = this.get( 0 ); |
676 | 1324 | if ( typeof description != 'object' ) { |
677 | 1325 | mw.log( "description should be an object, instead of a " + typeof description ); |
678 | 1326 | return null; |
— | — | @@ -679,32 +1327,26 @@ |
680 | 1328 | |
681 | 1329 | var $form = $( '<form/>' ).addClass( 'formbuilder' ); |
682 | 1330 | var prefix = options.gadget === undefined ? '' : ( 'Gadget-' + options.gadget + '-' ); |
683 | | - $form.data( 'formBuilder', { |
684 | | - prefix: prefix //prefix for messages |
685 | | - } ); |
686 | 1331 | |
687 | | - //If there is an "intro", adds it to the form as a label |
688 | | - if ( typeof description.intro == 'string' ) { |
689 | | - $( '<p/>' ) |
690 | | - .text( preproc( $form, description.intro ) ) |
691 | | - .addClass( 'formBuilder-intro' ) |
692 | | - .appendTo( $form ); |
693 | | - } |
694 | | - |
695 | 1332 | if ( typeof description.fields != 'object' ) { |
696 | 1333 | mw.log( "description.fields should be an object, instead of a " + typeof description.fields ); |
697 | 1334 | return null; |
698 | 1335 | } |
699 | 1336 | |
700 | | - var section = new SectionField( $form, description, options.values ); |
| 1337 | + var section = new SectionField( description, { |
| 1338 | + prefix: prefix, |
| 1339 | + values: options.values, |
| 1340 | + editable: options.editable === true |
| 1341 | + } ); |
701 | 1342 | |
702 | 1343 | section.getElement().appendTo( $form ); |
703 | 1344 | |
704 | 1345 | var validator = $form.validate( section.getValidationSettings() ); |
705 | 1346 | |
706 | | - var data = $form.data( 'formBuilder' ); |
707 | | - data.mainSection = section; |
708 | | - data.validator = validator; |
| 1347 | + $form.data( 'formBuilder', { |
| 1348 | + mainSection: section, |
| 1349 | + validator: validator |
| 1350 | + } ); |
709 | 1351 | |
710 | 1352 | return $form; |
711 | 1353 | } |
— | — | @@ -723,6 +1365,20 @@ |
724 | 1366 | }, |
725 | 1367 | |
726 | 1368 | /** |
| 1369 | + * Returns the current description, where current values as set for 'default' values. |
| 1370 | + * Used by the preference editor. |
| 1371 | + * |
| 1372 | + * NOTE: it is responsibility of the caller to call 'validate' and ensure that |
| 1373 | + * current values pass validation before calling this method. |
| 1374 | + * |
| 1375 | + * @return {Object} |
| 1376 | + */ |
| 1377 | + getDescription: function() { |
| 1378 | + var data = this.data( 'formBuilder' ); |
| 1379 | + return data.mainSection.getDesc( true ); |
| 1380 | + }, |
| 1381 | + |
| 1382 | + /** |
727 | 1383 | * Do validation of form fields and warn the user about wrong values, if any. |
728 | 1384 | * |
729 | 1385 | * @return {Boolean} true if all fields pass validation, false otherwise. |
— | — | @@ -731,7 +1387,6 @@ |
732 | 1388 | var data = this.data( 'formBuilder' ); |
733 | 1389 | return data.validator.form(); |
734 | 1390 | } |
735 | | - |
736 | 1391 | }; |
737 | 1392 | |
738 | 1393 | $.fn.formBuilder = function( method ) { |
Index: branches/salvatoreingala/Gadgets/ui/resources/ext.gadgets.preferences.js |
— | — | @@ -86,7 +86,7 @@ |
87 | 87 | resizable: false, |
88 | 88 | title: mw.msg( 'gadgets-configuration-of', gadget ), |
89 | 89 | close: function() { |
90 | | - $( this ).dialog( 'destroy' ).empty(); //completely destroy on close |
| 90 | + $( this ).remove(); |
91 | 91 | }, |
92 | 92 | buttons: [ |
93 | 93 | //TODO: add a "Restore defaults" button |