- add vim modeline
[phpmyadmin.git] / js / config.js
blob5b0abfcc816cdf1f22c36e9796617d769bf6f590
1 /* vim: set expandtab sw=4 ts=4 sts=4: */
2 /**
3  * Functions used in configuration forms and on user preferences pages
4  */
6 // default values for fields
7 var defaultValues = {};
9 // language strings
10 var PMA_messages = {};
12 /**
13  * Returns field type
14  *
15  * @param {Element} field
16  */
17 function getFieldType(field) {
18     field = $(field);
19     var tagName = field.attr('tagName');
20     if (tagName == 'INPUT') {
21         return field.attr('type');
22     } else if (tagName == 'SELECT') {
23         return 'select';
24     } else if (tagName == 'TEXTAREA') {
25         return 'text';
26     }
27     return '';
30 /**
31  * Sets field value
32  *
33  * value must be of type:
34  * o undefined (omitted) - restore default value (form default, not PMA default)
35  * o String - if field_type is 'text'
36  * o boolean - if field_type is 'checkbox'
37  * o Array of values - if field_type is 'select'
38  *
39  * @param {Element} field
40  * @param {String}  field_type  see {@link #getFieldType}
41  * @param {String|Boolean}  [value]
42  */
43 function setFieldValue(field, field_type, value) {
44     field = $(field);
45     switch (field_type) {
46         case 'text':
47             field.attr('value', (value != undefined ? value : field.attr('defaultValue')));
48             break;
49         case 'checkbox':
50             field.attr('checked', (value != undefined ? value : field.attr('defaultChecked')));
51             break;
52         case 'select':
53             var options = field.attr('options');
54             var i, imax = options.length;
55             if (value == undefined) {
56                 for (i = 0; i < imax; i++) {
57                     options[i].selected = options[i].defaultSelected;
58                 }
59             } else {
60                 for (i = 0; i < imax; i++) {
61                     options[i].selected = (value.indexOf(options[i].value) != -1);
62                 }
63             }
64             break;
65     }
66     markField(field);
69 /**
70  * Gets field value
71  *
72  * Will return one of:
73  * o String - if type is 'text'
74  * o boolean - if type is 'checkbox'
75  * o Array of values - if type is 'select'
76  *
77  * @param {Element} field
78  * @param {String}  field_type returned by {@link #getFieldType}
79  * @type Boolean|String|String[]
80  */
81 function getFieldValue(field, field_type) {
82     field = $(field);
83     switch (field_type) {
84         case 'text':
85             return field.attr('value');
86         case 'checkbox':
87             return field.attr('checked');
88         case 'select':
89             var options = field.attr('options');
90             var i, imax = options.length, items = [];
91             for (i = 0; i < imax; i++) {
92                 if (options[i].selected) {
93                     items.push(options[i].value);
94                 }
95             }
96             return items;
97     }
98     return null;
102  * Returns values for all fields in fieldsets
103  */
104 function getAllValues() {
105     var elements = $('fieldset input, fieldset select, fieldset textarea');
106     var values = {};
107     var type, value;
108     for (var i = 0; i < elements.length; i++) {
109         type = getFieldType(elements[i]);
110         value = getFieldValue(elements[i], type);
111         if (typeof value != 'undefined') {
112             // we only have single selects, fatten array
113             if (type == 'select') {
114                 value = value[0];
115             }
116             values[elements[i].name] = value;
117         }
118     }
119     return values;
123  * Checks whether field has its default value
125  * @param {Element} field
126  * @param {String}  type
127  * @return boolean
128  */
129 function checkFieldDefault(field, type) {
130     field = $(field);
131     var field_id = field.attr('id');
132     if (typeof defaultValues[field_id] == 'undefined') {
133         return true;
134     }
135     var isDefault = true;
136     var currentValue = getFieldValue(field, type);
137     if (type != 'select') {
138         isDefault = currentValue == defaultValues[field_id];
139     } else {
140         // compare arrays, will work for our representation of select values
141         if (currentValue.length != defaultValues[field_id].length) {
142             isDefault = false;
143         }
144         else {
145             for (var i = 0; i < currentValue.length; i++) {
146                 if (currentValue[i] != defaultValues[field_id][i]) {
147                     isDefault = false;
148                     break;
149                 }
150             }
151         }
152     }
153     return isDefault;
157  * Returns element's id prefix
158  * @param {Element} element
159  */
160 function getIdPrefix(element) {
161     return $(element).attr('id').replace(/[^-]+$/, '');
164 // ------------------------------------------------------------------
165 // Form validation and field operations
168 // form validator assignments
169 var validate = {};
171 // form validator list
172 var validators = {
173     // regexp: numeric value
174     _regexp_numeric: /^[0-9]+$/,
175     // regexp: extract parts from PCRE expression
176     _regexp_pcre_extract: /(.)(.*)\1(.*)?/,
177     /**
178      * Validates positive number
179      *
180      * @param {boolean} isKeyUp
181      */
182     validate_positive_number: function (isKeyUp) {
183         if (isKeyUp && this.value == '') {
184             return true;
185         }
186         var result = this.value != '0' && validators._regexp_numeric.test(this.value);
187         return result ? true : PMA_messages['error_nan_p'];
188     },
189     /**
190      * Validates non-negative number
191      *
192      * @param {boolean} isKeyUp
193      */
194     validate_non_negative_number: function (isKeyUp) {
195         if (isKeyUp && this.value == '') {
196             return true;
197         }
198         var result = validators._regexp_numeric.test(this.value);
199         return result ? true : PMA_messages['error_nan_nneg'];
200     },
201     /**
202      * Validates port number
203      *
204      * @param {boolean} isKeyUp
205      */
206     validate_port_number: function(isKeyUp) {
207         if (this.value == '') {
208             return true;
209         }
210         var result = validators._regexp_numeric.test(this.value) && this.value != '0';
211         return result && this.value <= 65535 ? true : PMA_messages['error_incorrect_port'];
212     },
213     /**
214      * Validates value according to given regular expression
215      *
216      * @param {boolean} isKeyUp
217      * @param {string}  regexp
218      */
219     validate_by_regex: function(isKeyUp, regexp) {
220         if (isKeyUp && this.value == '') {
221             return true;
222         }
223         // convert PCRE regexp
224         var parts = regexp.match(validators._regexp_pcre_extract);
225         var valid = this.value.match(new RegExp(parts[2], parts[3])) != null;
226         return valid ? true : PMA_messages['error_invalid_value'];
227     },
228     /**
229      * Validates upper bound for numeric inputs
230      *
231      * @param {boolean} isKeyUp
232      * @param {int} max_value
233      */
234     validate_upper_bound: function(isKeyUp, max_value) {
235         var val = parseInt(this.value);
236         if (isNaN(val)) {
237             return true;
238         }
239         return val <= max_value ? true : PMA_messages['error_value_lte'].replace('%s', max_value);
240     },
241     // field validators
242     _field: {
243     },
244     // fieldset validators
245     _fieldset: {
246     }
250  * Registers validator for given field
252  * @param {String}  id       field id
253  * @param {String}  type     validator (key in validators object)
254  * @param {boolean} onKeyUp  whether fire on key up
255  * @param {Array}   params   validation function parameters
256  */
257 function validateField(id, type, onKeyUp, params) {
258     if (typeof validators[type] == 'undefined') {
259         return;
260     }
261     if (typeof validate[id] == 'undefined') {
262         validate[id] = [];
263     }
264     validate[id].push([type, params, onKeyUp]);
268  * Returns valdiation functions associated with form field
270  * @param  {String}  field_id     form field id
271  * @param  {boolean} onKeyUpOnly  see validateField
272  * @type Array
273  * @return array of [function, paramseters to be passed to function]
274  */
275 function getFieldValidators(field_id, onKeyUpOnly) {
276     // look for field bound validator
277     var name = field_id.match(/[^-]+$/)[0];
278     if (typeof validators._field[name] != 'undefined') {
279         return [[validators._field[name], null]];
280     }
282     // look for registered validators
283     var functions = [];
284     if (typeof validate[field_id] != 'undefined') {
285         // validate[field_id]: array of [type, params, onKeyUp]
286         for (var i = 0, imax = validate[field_id].length; i < imax; i++) {
287             if (onKeyUpOnly && !validate[field_id][i][2]) {
288                 continue;
289             }
290             functions.push([validators[validate[field_id][i][0]], validate[field_id][i][1]]);
291         }
292     }
294     return functions;
298  * Displays errors for given form fields
300  * WARNING: created DOM elements must be identical with the ones made by
301  * display_input() in FormDisplay.tpl.php!
303  * @param {Object} error_list list of errors in the form {field id: error array}
304  */
305 function displayErrors(error_list) {
306     for (var field_id in error_list) {
307         var errors = error_list[field_id];
308         var field = $('#'+field_id);
309         var isFieldset = field.attr('tagName') == 'FIELDSET';
310         var errorCnt = isFieldset
311             ? field.find('dl.errors')
312             : field.siblings('.inline_errors');
314         // remove empty errors (used to clear error list)
315         errors = $.grep(errors, function(item) {
316             return item != '';
317         });
319         // CSS error class
320         if (!isFieldset) {
321             // checkboxes uses parent <span> for marking
322             var fieldMarker = (field.attr('type') == 'checkbox') ? field.parent() : field;
323             fieldMarker[errors.length ? 'addClass' : 'removeClass']('field-error');
324         }
326         if (errors.length) {
327             // if error container doesn't exist, create it
328             if (errorCnt.length == 0) {
329                 if (isFieldset) {
330                     errorCnt = $('<dl class="errors" />');
331                     field.find('table').before(errorCnt);
332                 } else {
333                     errorCnt = $('<dl class="inline_errors" />');
334                     field.closest('td').append(errorCnt);
335                 }
336             }
338             var html = '';
339             for (var i = 0, imax = errors.length; i < imax; i++) {
340                 html += '<dd>' + errors[i] + '</dd>';
341             }
342             errorCnt.html(html);
343         } else if (errorCnt !== null) {
344             // remove useless error container
345             errorCnt.remove();
346         }
347     }
351  * Validates fieldset and puts errors in 'errors' object
353  * @param {Element} fieldset
354  * @param {boolean} isKeyUp
355  * @param {Object}  errors
356  */
357 function validate_fieldset(fieldset, isKeyUp, errors) {
358     fieldset = $(fieldset);
359     if (fieldset.length && typeof validators._fieldset[fieldset.attr('id')] != 'undefined') {
360         var fieldset_errors = validators._fieldset[fieldset.attr('id')].apply(fieldset[0], [isKeyUp]);
361         for (var field_id in fieldset_errors) {
362             if (typeof errors[field_id] == 'undefined') {
363                 errors[field_id] = [];
364             }
365             if (typeof fieldset_errors[field_id] == 'string') {
366                 fieldset_errors[field_id] = [fieldset_errors[field_id]];
367             }
368             $.merge(errors[field_id], fieldset_errors[field_id]);
369         }
370     }
374  * Validates form field and puts errors in 'errors' object
376  * @param {Element} field
377  * @param {boolean} isKeyUp
378  * @param {Object}  errors
379  */
380 function validate_field(field, isKeyUp, errors) {
381     field = $(field);
382     var field_id = field.attr('id');
383     errors[field_id] = [];
384     var functions = getFieldValidators(field_id, isKeyUp);
385     for (var i = 0; i < functions.length; i++) {
386         var args = functions[i][1] != null
387             ? functions[i][1].slice(0)
388             : [];
389         args.unshift(isKeyUp);
390         var result = functions[i][0].apply(field[0], args);
391         if (result !== true) {
392             if (typeof result == 'string') {
393                 result = [result];
394             }
395             $.merge(errors[field_id], result);
396         }
397     }
401  * Validates form field and parent fieldset
403  * @param {Element} field
404  * @param {boolean} isKeyUp
405  */
406 function validate_field_and_fieldset(field, isKeyUp) {
407     field = $(field);
408     var errors = {};
409     validate_field(field, isKeyUp, errors);
410     validate_fieldset(field.closest('fieldset'), isKeyUp, errors);
411     displayErrors(errors);
415  * Marks field depending on its value (system default or custom)
417  * @param {Element} field
418  */
419 function markField(field) {
420     field = $(field);
421     var type = getFieldType(field);
422     var isDefault = checkFieldDefault(field, type);
424     // checkboxes uses parent <span> for marking
425     var fieldMarker = (type == 'checkbox') ? field.parent() : field;
426     setRestoreDefaultBtn(field, !isDefault);
427     fieldMarker[isDefault ? 'removeClass' : 'addClass']('custom');
431  * Enables or disables the "restore default value" button
433  * @param {Element} field
434  * @param {boolean} display
435  */
436 function setRestoreDefaultBtn(field, display) {
437     var el = $(field).closest('td').find('.restore-default img');
438     el[display ? 'show' : 'hide']();
441 $(function() {
442     // register validators and mark custom values
443     var elements = $('input[id], select[id], textarea[id]');
444     $('input[id], select[id], textarea[id]').each(function(){
445         markField(this);
446         var el = $(this);
447         el.bind('change', function() {
448             validate_field_and_fieldset(this, false);
449             markField(this);
450         });
451         var tagName = el.attr('tagName');
452         // text fields can be validated after each change
453         if (tagName == 'INPUT' && el.attr('type') == 'text') {
454             el.keyup(function() {
455                 validate_field_and_fieldset(el, true);
456                 markField(el);
457             });
458         }
459         // disable textarea spellcheck
460         if (tagName == 'TEXTAREA') {
461             el.attr('spellcheck', false);
462         }
463     });
465     // check whether we've refreshed a page and browser remembered modified
466     // form values
467     var check_page_refresh = $('#check_page_refresh');
468     if (check_page_refresh.length == 0 || check_page_refresh.val() == '1') {
469         // run all field validators
470         var errors = {};
471         for (var i = 0; i < elements.length; i++) {
472             validate_field(elements[i], false, errors);
473         }
474         // run all fieldset validators
475         $('fieldset').each(function(){
476             validate_fieldset(this, false, errors);
477         });
479         displayErrors(errors);
480     } else if (check_page_refresh) {
481         check_page_refresh.val('1');
482     }
486 // END: Form validation and field operations
487 // ------------------------------------------------------------------
489 // ------------------------------------------------------------------
490 // Tabbed forms
494  * Sets active tab
496  * @param {String} tab_id
497  */
498 function setTab(tab_id) {
499     $('.tabs a').removeClass('active').filter('[href=' + tab_id + ']').addClass('active');
500     $('.tabs_contents fieldset').hide().filter(tab_id).show();
501     location.hash = 'tab_' + tab_id.substr(1);
502     $('.config-form input[name=tab_hash]').val(location.hash);
505 $(function() {
506     var tabs = $('.tabs');
507     if (!tabs.length) {
508         return;
509     }
510     // add tabs events and activate one tab (the first one or indicated by location hash)
511     tabs.find('a')
512         .click(function(e) {
513             e.preventDefault();
514             setTab($(this).attr('href'));
515         })
516         .filter(':first')
517         .addClass('active');
518     $('.tabs_contents fieldset').hide().filter(':first').show();
520     // tab links handling, check each 200ms
521     // (works with history in FF, further browser support here would be an overkill)
522     var prev_hash;
523     var tab_check_fnc = function() {
524         if (location.hash != prev_hash) {
525             prev_hash = location.hash;
526             if (location.hash.match(/^#tab_.+/) && $('#' + location.hash.substr(5)).length) {
527                 setTab('#' + location.hash.substr(5));
528             }
529         }
530     };
531     tab_check_fnc();
532     setInterval(tab_check_fnc, 200);
536 // END: Tabbed forms
537 // ------------------------------------------------------------------
539 // ------------------------------------------------------------------
540 // Form reset buttons
543 $(function() {
544     $('input[type=button][name=submit_reset]').click(function() {
545         var fields = $(this).closest('fieldset').find('input, select, textarea');
546         for (var i = 0, imax = fields.length; i < imax; i++) {
547             setFieldValue(fields[i], getFieldType(fields[i]));
548         }
549     });
553 // END: Form reset buttons
554 // ------------------------------------------------------------------
556 // ------------------------------------------------------------------
557 // "Restore default" and "set value" buttons
561  * Restores field's default value
563  * @param {String} field_id
564  */
565 function restoreField(field_id) {
566     var field = $('#'+field_id);
567     if (field.length == 0 || defaultValues[field_id] == undefined) {
568         return;
569     }
570     setFieldValue(field, getFieldType(field), defaultValues[field_id]);
573 $(function() {
574     $('.tabs_contents')
575         .delegate('.restore-default, .set-value', 'mouseenter', function(){$(this).css('opacity', 1)})
576         .delegate('.restore-default, .set-value', 'mouseleave', function(){$(this).css('opacity', 0.25)})
577         .delegate('.restore-default, .set-value', 'click', function(e) {
578             e.preventDefault();
579             var href = $(this).attr('href');
580             var field_sel;
581             if ($(this).hasClass('restore-default')) {
582                 field_sel = href;
583                 restoreField(field_sel.substr(1));
584             } else {
585                 field_sel = href.match(/^[^=]+/)[0];
586                 var value = href.match(/=(.+)$/)[1];
587                 setFieldValue($(field_sel), 'text', value);
588             }
589             $(field_sel).trigger('change');
590         })
591         .find('.restore-default, .set-value')
592         // inline-block for IE so opacity inheritance works
593         .css({display: 'inline-block', opacity: 0.25});
597 // END: "Restore default" and "set value" buttons
598 // ------------------------------------------------------------------
600 // ------------------------------------------------------------------
601 // User preferences import/export
604 $(function() {
605     offerPrefsAutoimport();
606     var radios = $('#import_local_storage, #export_local_storage');
607     if (!radios.length) {
608         return;
609     }
611     // enable JavaScript dependent fields
612     radios
613         .attr('disabled', false)
614         .add('#export_text_file, #import_text_file')
615         .click(function(){
616             var enable_id = $(this).attr('id');
617             var disable_id = enable_id.match(/local_storage$/)
618                 ? enable_id.replace(/local_storage$/, 'text_file')
619                 : enable_id.replace(/text_file$/, 'local_storage');
620             $('#opts_'+disable_id).addClass('disabled').find('input').attr('disabled', true);
621             $('#opts_'+enable_id).removeClass('disabled').find('input').attr('disabled', false);
622         });
624     // detect localStorage state
625     var ls_supported = window.localStorage || false;
626     var ls_exists = ls_supported ? (window.localStorage['config'] || false) : false;
627     $('.localStorage-'+(ls_supported ? 'un' : '')+'supported').hide();
628     $('.localStorage-'+(ls_exists ? 'empty' : 'exists')).hide();
629     if (ls_exists) {
630         updatePrefsDate();
631     }
632     $('form.prefs-form').change(function(){
633         var form = $(this);
634         var disabled = false;
635         if (!ls_supported) {
636             disabled = form.find('input[type=radio][value$=local_storage]').attr('checked');
637         } else if (!ls_exists && form.attr('name') == 'prefs_import'
638                 && $('#import_local_storage')[0].checked) {
639             disabled = true;
640         }
641         form.find('input[type=submit]').attr('disabled', disabled);
642     }).submit(function(e) {
643         var form = $(this);
644         if (form.attr('name') == 'prefs_export' && $('#export_local_storage')[0].checked) {
645             e.preventDefault();
646             // use AJAX to read JSON settings and save them
647             savePrefsToLocalStorage(form);
648         } else if (form.attr('name') == 'prefs_import' && $('#import_local_storage')[0].checked) {
649             // set 'json' input and submit form
650             form.find('input[name=json]').val(window.localStorage['config']);
651         }
652     });
654     $('.click-hide-message').live('click', function(){
655         var div = $(this);
656         div.hide().parent('.group').css('height', '');
657         div.next('form').show();
658     });
662  * Saves user preferences to localStorage
664  * @param {Element} form
665  */
666 function savePrefsToLocalStorage(form)
668     form = $(form);
669     var submit = form.find('input[type=submit]');
670     submit.attr('disabled', true);
671     $.ajax({
672         url: 'prefs_manage.php',
673         cache: false,
674         type: 'POST',
675         data: {
676             token: form.find('input[name=token]').val(),
677             submit_get_json: true
678         },
679         success: function(response) {
680             window.localStorage['config'] = response.prefs;
681             window.localStorage['config_mtime'] = response.mtime;
682             window.localStorage['config_mtime_local'] = (new Date()).toUTCString();
683             updatePrefsDate();
684             $('.localStorage-empty').hide();
685             $('.localStorage-exists').show();
686             var group = form.parent('.group');
687             group.css('height', group.height() + 'px');
688             form.hide('fast');
689             form.prev('.click-hide-message').show('fast');
690         },
691         complete: function() {
692             submit.attr('disabled', false);
693         }
694     });
698  * Updates preferences timestamp in Import form
699  */
700 function updatePrefsDate()
702     var d = new Date(window.localStorage['config_mtime_local']);
703     var msg = PMA_messages['strSavedOn'].replace('@DATE@', formatDate(d));
704     $('#opts_import_local_storage .localStorage-exists').html(msg);
708  * Returns date formatted as YYYY-MM-DD HH:II
710  * @param {Date} d
711  */
712 function formatDate(d)
714     return d.getFullYear() + '-'
715         + (d.getMonth() < 10 ? '0'+d.getMonth() : d.getMonth())
716         + '-' + (d.getDate() < 10 ? '0'+d.getDate() : d.getDate())
717         + ' ' + (d.getHours() < 10 ? '0'+d.getHours() : d.getHours())
718         + ':' + (d.getMinutes() < 10 ? '0'+d.getMinutes() : d.getMinutes());
722  * Prepares message which informs that localStorage preferences are available and can be imported
723  */
724 function offerPrefsAutoimport()
726     var has_config = (window.localStorage || false) && (window.localStorage['config'] || false);
727     var cnt = $('#prefs_autoload');
728     if (!cnt.length || !has_config) {
729         return;
730     }
731     cnt.find('a').click(function(e) {
732         e.preventDefault();
733         var a = $(this);
734         if (a.attr('href') == '#no') {
735             cnt.remove();
736             $.post('main.php', {
737                 token: cnt.find('input[name=token]').val(),
738                 prefs_autoload: 'hide'});
739             return;
740         }
741         cnt.find('input[name=json]').val(window.localStorage['config']);
742         cnt.find('form').submit();
743     });
744     cnt.show();
748 // END: User preferences import/export
749 // ------------------------------------------------------------------