bug #4002 Edit Index on big table does not show "Loading" or any message
[phpmyadmin.git] / js / config.js
blobfdd0fc4ad7fa92d3e08ea8176ead0817255bbd3c
1 /* vim: set expandtab sw=4 ts=4 sts=4: */
2 /**
3  * Functions used in configuration forms and on user preferences pages
4  */
6 /**
7  * Unbind all event handlers before tearing down a page
8  */
9 AJAX.registerTeardown('config.js', function() {
10     $('input[id], select[id], textarea[id]').unbind('change').unbind('keyup');
11     $('input[type=button][name=submit_reset]').unbind('click');
12     $('div.tabs_contents').undelegate();
13     $('#import_local_storage, #export_local_storage').unbind('click');
14     $('form.prefs-form').unbind('change').unbind('submit');
15     $('div.click-hide-message').die('click');
16     $('#prefs_autoload').find('a').unbind('click');
17 });
19 AJAX.registerOnload('config.js', function() {
20     $('#topmenu2').find('li.active a').attr('rel', 'samepage');
21     $('#topmenu2').find('li:not(.active) a').attr('rel', 'newpage');
22 });
24 // default values for fields
25 var defaultValues = {};
27 /**
28  * Returns field type
29  *
30  * @param {Element} field
31  */
32 function getFieldType(field)
34     field = $(field);
35     var tagName = field.prop('tagName');
36     if (tagName == 'INPUT') {
37         return field.attr('type');
38     } else if (tagName == 'SELECT') {
39         return 'select';
40     } else if (tagName == 'TEXTAREA') {
41         return 'text';
42     }
43     return '';
46 /**
47  * Sets field value
48  *
49  * value must be of type:
50  * o undefined (omitted) - restore default value (form default, not PMA default)
51  * o String - if field_type is 'text'
52  * o boolean - if field_type is 'checkbox'
53  * o Array of values - if field_type is 'select'
54  *
55  * @param {Element} field
56  * @param {String}  field_type  see {@link #getFieldType}
57  * @param {String|Boolean}  [value]
58  */
59 function setFieldValue(field, field_type, value)
61     field = $(field);
62     switch (field_type) {
63         case 'text':
64             //TODO: replace to .val()
65             field.attr('value', (value != undefined ? value : field.attr('defaultValue')));
66             break;
67         case 'checkbox':
68             //TODO: replace to .prop()
69             field.attr('checked', (value != undefined ? value : field.attr('defaultChecked')));
70             break;
71         case 'select':
72             var options = field.prop('options');
73             var i, imax = options.length;
74             if (value == undefined) {
75                 for (i = 0; i < imax; i++) {
76                     options[i].selected = options[i].defaultSelected;
77                 }
78             } else {
79                 for (i = 0; i < imax; i++) {
80                     options[i].selected = (value.indexOf(options[i].value) != -1);
81                 }
82             }
83             break;
84     }
85     markField(field);
88 /**
89  * Gets field value
90  *
91  * Will return one of:
92  * o String - if type is 'text'
93  * o boolean - if type is 'checkbox'
94  * o Array of values - if type is 'select'
95  *
96  * @param {Element} field
97  * @param {String}  field_type returned by {@link #getFieldType}
98  * @type Boolean|String|String[]
99  */
100 function getFieldValue(field, field_type)
102     field = $(field);
103     switch (field_type) {
104         case 'text':
105             return field.prop('value');
106         case 'checkbox':
107             return field.prop('checked');
108         case 'select':
109             var options = field.prop('options');
110             var i, imax = options.length, items = [];
111             for (i = 0; i < imax; i++) {
112                 if (options[i].selected) {
113                     items.push(options[i].value);
114                 }
115             }
116             return items;
117     }
118     return null;
122  * Returns values for all fields in fieldsets
123  */
124 function getAllValues()
126     var elements = $('fieldset input, fieldset select, fieldset textarea');
127     var values = {};
128     var type, value;
129     for (var i = 0; i < elements.length; i++) {
130         type = getFieldType(elements[i]);
131         value = getFieldValue(elements[i], type);
132         if (typeof value != 'undefined') {
133             // we only have single selects, fatten array
134             if (type == 'select') {
135                 value = value[0];
136             }
137             values[elements[i].name] = value;
138         }
139     }
140     return values;
144  * Checks whether field has its default value
146  * @param {Element} field
147  * @param {String}  type
148  * @return boolean
149  */
150 function checkFieldDefault(field, type)
152     field = $(field);
153     var field_id = field.attr('id');
154     if (typeof defaultValues[field_id] == 'undefined') {
155         return true;
156     }
157     var isDefault = true;
158     var currentValue = getFieldValue(field, type);
159     if (type != 'select') {
160         isDefault = currentValue == defaultValues[field_id];
161     } else {
162         // compare arrays, will work for our representation of select values
163         if (currentValue.length != defaultValues[field_id].length) {
164             isDefault = false;
165         }
166         else {
167             for (var i = 0; i < currentValue.length; i++) {
168                 if (currentValue[i] != defaultValues[field_id][i]) {
169                     isDefault = false;
170                     break;
171                 }
172             }
173         }
174     }
175     return isDefault;
179  * Returns element's id prefix
180  * @param {Element} element
181  */
182 function getIdPrefix(element)
184     return $(element).attr('id').replace(/[^-]+$/, '');
187 // ------------------------------------------------------------------
188 // Form validation and field operations
191 // form validator assignments
192 var validate = {};
194 // form validator list
195 var validators = {
196     // regexp: numeric value
197     _regexp_numeric: /^[0-9]+$/,
198     // regexp: extract parts from PCRE expression
199     _regexp_pcre_extract: /(.)(.*)\1(.*)?/,
200     /**
201      * Validates positive number
202      *
203      * @param {boolean} isKeyUp
204      */
205     validate_positive_number: function (isKeyUp) {
206         if (isKeyUp && this.value == '') {
207             return true;
208         }
209         var result = this.value != '0' && validators._regexp_numeric.test(this.value);
210         return result ? true : PMA_messages['error_nan_p'];
211     },
212     /**
213      * Validates non-negative number
214      *
215      * @param {boolean} isKeyUp
216      */
217     validate_non_negative_number: function (isKeyUp) {
218         if (isKeyUp && this.value == '') {
219             return true;
220         }
221         var result = validators._regexp_numeric.test(this.value);
222         return result ? true : PMA_messages['error_nan_nneg'];
223     },
224     /**
225      * Validates port number
226      *
227      * @param {boolean} isKeyUp
228      */
229     validate_port_number: function(isKeyUp) {
230         if (this.value == '') {
231             return true;
232         }
233         var result = validators._regexp_numeric.test(this.value) && this.value != '0';
234         return result && this.value <= 65535 ? true : PMA_messages['error_incorrect_port'];
235     },
236     /**
237      * Validates value according to given regular expression
238      *
239      * @param {boolean} isKeyUp
240      * @param {string}  regexp
241      */
242     validate_by_regex: function(isKeyUp, regexp) {
243         if (isKeyUp && this.value == '') {
244             return true;
245         }
246         // convert PCRE regexp
247         var parts = regexp.match(validators._regexp_pcre_extract);
248         var valid = this.value.match(new RegExp(parts[2], parts[3])) != null;
249         return valid ? true : PMA_messages['error_invalid_value'];
250     },
251     /**
252      * Validates upper bound for numeric inputs
253      *
254      * @param {boolean} isKeyUp
255      * @param {int} max_value
256      */
257     validate_upper_bound: function(isKeyUp, max_value) {
258         var val = parseInt(this.value);
259         if (isNaN(val)) {
260             return true;
261         }
262         return val <= max_value ? true : $.sprintf(PMA_messages['error_value_lte'], max_value);
263     },
264     // field validators
265     _field: {
266     },
267     // fieldset validators
268     _fieldset: {
269     }
273  * Registers validator for given field
275  * @param {String}  id       field id
276  * @param {String}  type     validator (key in validators object)
277  * @param {boolean} onKeyUp  whether fire on key up
278  * @param {Array}   params   validation function parameters
279  */
280 function validateField(id, type, onKeyUp, params)
282     if (typeof validators[type] == 'undefined') {
283         return;
284     }
285     if (typeof validate[id] == 'undefined') {
286         validate[id] = [];
287     }
288     validate[id].push([type, params, onKeyUp]);
292  * Returns valdiation functions associated with form field
294  * @param {String}  field_id     form field id
295  * @param {boolean} onKeyUpOnly  see validateField
296  * @type Array
297  * @return array of [function, paramseters to be passed to function]
298  */
299 function getFieldValidators(field_id, onKeyUpOnly)
301     // look for field bound validator
302     var name = field_id.match(/[^-]+$/)[0];
303     if (typeof validators._field[name] != 'undefined') {
304         return [[validators._field[name], null]];
305     }
307     // look for registered validators
308     var functions = [];
309     if (typeof validate[field_id] != 'undefined') {
310         // validate[field_id]: array of [type, params, onKeyUp]
311         for (var i = 0, imax = validate[field_id].length; i < imax; i++) {
312             if (onKeyUpOnly && !validate[field_id][i][2]) {
313                 continue;
314             }
315             functions.push([validators[validate[field_id][i][0]], validate[field_id][i][1]]);
316         }
317     }
319     return functions;
323  * Displays errors for given form fields
325  * WARNING: created DOM elements must be identical with the ones made by
326  * display_input() in FormDisplay.tpl.php!
328  * @param {Object} error_list list of errors in the form {field id: error array}
329  */
330 function displayErrors(error_list)
332     for (var field_id in error_list) {
333         var errors = error_list[field_id];
334         var field = $('#'+field_id);
335         var isFieldset = field.attr('tagName') == 'FIELDSET';
336         var errorCnt = isFieldset
337             ? field.find('dl.errors')
338             : field.siblings('.inline_errors');
340         // remove empty errors (used to clear error list)
341         errors = $.grep(errors, function(item) {
342             return item != '';
343         });
345         // CSS error class
346         if (!isFieldset) {
347             // checkboxes uses parent <span> for marking
348             var fieldMarker = (field.attr('type') == 'checkbox') ? field.parent() : field;
349             fieldMarker[errors.length ? 'addClass' : 'removeClass']('field-error');
350         }
352         if (errors.length) {
353             // if error container doesn't exist, create it
354             if (errorCnt.length == 0) {
355                 if (isFieldset) {
356                     errorCnt = $('<dl class="errors" />');
357                     field.find('table').before(errorCnt);
358                 } else {
359                     errorCnt = $('<dl class="inline_errors" />');
360                     field.closest('td').append(errorCnt);
361                 }
362             }
364             var html = '';
365             for (var i = 0, imax = errors.length; i < imax; i++) {
366                 html += '<dd>' + errors[i] + '</dd>';
367             }
368             errorCnt.html(html);
369         } else if (errorCnt !== null) {
370             // remove useless error container
371             errorCnt.remove();
372         }
373     }
377  * Validates fieldset and puts errors in 'errors' object
379  * @param {Element} fieldset
380  * @param {boolean} isKeyUp
381  * @param {Object}  errors
382  */
383 function validate_fieldset(fieldset, isKeyUp, errors)
385     fieldset = $(fieldset);
386     if (fieldset.length && typeof validators._fieldset[fieldset.attr('id')] != 'undefined') {
387         var fieldset_errors = validators._fieldset[fieldset.attr('id')].apply(fieldset[0], [isKeyUp]);
388         for (var field_id in fieldset_errors) {
389             if (typeof errors[field_id] == 'undefined') {
390                 errors[field_id] = [];
391             }
392             if (typeof fieldset_errors[field_id] == 'string') {
393                 fieldset_errors[field_id] = [fieldset_errors[field_id]];
394             }
395             $.merge(errors[field_id], fieldset_errors[field_id]);
396         }
397     }
401  * Validates form field and puts errors in 'errors' object
403  * @param {Element} field
404  * @param {boolean} isKeyUp
405  * @param {Object}  errors
406  */
407 function validate_field(field, isKeyUp, errors)
409     field = $(field);
410     var field_id = field.attr('id');
411     errors[field_id] = [];
412     var functions = getFieldValidators(field_id, isKeyUp);
413     for (var i = 0; i < functions.length; i++) {
414         var args = functions[i][1] != null
415             ? functions[i][1].slice(0)
416             : [];
417         args.unshift(isKeyUp);
418         var result = functions[i][0].apply(field[0], args);
419         if (result !== true) {
420             if (typeof result == 'string') {
421                 result = [result];
422             }
423             $.merge(errors[field_id], result);
424         }
425     }
429  * Validates form field and parent fieldset
431  * @param {Element} field
432  * @param {boolean} isKeyUp
433  */
434 function validate_field_and_fieldset(field, isKeyUp)
436     field = $(field);
437     var errors = {};
438     validate_field(field, isKeyUp, errors);
439     validate_fieldset(field.closest('fieldset'), isKeyUp, errors);
440     displayErrors(errors);
444  * Marks field depending on its value (system default or custom)
446  * @param {Element} field
447  */
448 function markField(field)
450     field = $(field);
451     var type = getFieldType(field);
452     var isDefault = checkFieldDefault(field, type);
454     // checkboxes uses parent <span> for marking
455     var fieldMarker = (type == 'checkbox') ? field.parent() : field;
456     setRestoreDefaultBtn(field, !isDefault);
457     fieldMarker[isDefault ? 'removeClass' : 'addClass']('custom');
461  * Enables or disables the "restore default value" button
463  * @param {Element} field
464  * @param {boolean} display
465  */
466 function setRestoreDefaultBtn(field, display)
468     var el = $(field).closest('td').find('.restore-default img');
469     el[display ? 'show' : 'hide']();
472 AJAX.registerOnload('config.js', function() {
473     // register validators and mark custom values
474     var elements = $('input[id], select[id], textarea[id]');
475     $('input[id], select[id], textarea[id]').each(function(){
476         markField(this);
477         var el = $(this);
478         el.bind('change', function() {
479             validate_field_and_fieldset(this, false);
480             markField(this);
481         });
482         var tagName = el.attr('tagName');
483         // text fields can be validated after each change
484         if (tagName == 'INPUT' && el.attr('type') == 'text') {
485             el.keyup(function() {
486                 validate_field_and_fieldset(el, true);
487                 markField(el);
488             });
489         }
490         // disable textarea spellcheck
491         if (tagName == 'TEXTAREA') {
492            el.attr('spellcheck', false);
493         }
494     });
496     // check whether we've refreshed a page and browser remembered modified
497     // form values
498     var check_page_refresh = $('#check_page_refresh');
499     if (check_page_refresh.length == 0 || check_page_refresh.val() == '1') {
500         // run all field validators
501         var errors = {};
502         for (var i = 0; i < elements.length; i++) {
503             validate_field(elements[i], false, errors);
504         }
505         // run all fieldset validators
506         $('fieldset').each(function(){
507             validate_fieldset(this, false, errors);
508         });
510         displayErrors(errors);
511     } else if (check_page_refresh) {
512         check_page_refresh.val('1');
513     }
517 // END: Form validation and field operations
518 // ------------------------------------------------------------------
520 // ------------------------------------------------------------------
521 // Tabbed forms
525  * Sets active tab
527  * @param {String} tab_id
528  */
529 function setTab(tab_id)
531     $('ul.tabs li').removeClass('active').find('a[href=#' + tab_id + ']').parent().addClass('active');
532     $('div.tabs_contents fieldset').hide().filter('#' + tab_id).show();
533     location.hash = 'tab_' + tab_id;
534     $('form.config-form input[name=tab_hash]').val(location.hash);
537 AJAX.registerOnload('config.js', function() {
538     var tabs = $('ul.tabs');
539     if (!tabs.length) {
540         return;
541     }
542     // add tabs events and activate one tab (the first one or indicated by location hash)
543     tabs.find('a')
544         .click(function(e) {
545             e.preventDefault();
546             setTab($(this).attr('href').substr(1));
547         })
548         .filter(':first')
549         .parent()
550         .addClass('active');
551     $('div.tabs_contents fieldset').hide().filter(':first').show();
553     // tab links handling, check each 200ms
554     // (works with history in FF, further browser support here would be an overkill)
555     var prev_hash;
556     var tab_check_fnc = function() {
557         if (location.hash != prev_hash) {
558             prev_hash = location.hash;
559             if (location.hash.match(/^#tab_.+/) && $('#' + location.hash.substr(5)).length) {
560                 setTab(location.hash.substr(5));
561             }
562         }
563     };
564     tab_check_fnc();
565     setInterval(tab_check_fnc, 200);
569 // END: Tabbed forms
570 // ------------------------------------------------------------------
572 // ------------------------------------------------------------------
573 // Form reset buttons
576 AJAX.registerOnload('config.js', function() {
577     $('input[type=button][name=submit_reset]').click(function() {
578         var fields = $(this).closest('fieldset').find('input, select, textarea');
579         for (var i = 0, imax = fields.length; i < imax; i++) {
580             setFieldValue(fields[i], getFieldType(fields[i]));
581         }
582     });
586 // END: Form reset buttons
587 // ------------------------------------------------------------------
589 // ------------------------------------------------------------------
590 // "Restore default" and "set value" buttons
594  * Restores field's default value
596  * @param {String} field_id
597  */
598 function restoreField(field_id)
600     var field = $('#'+field_id);
601     if (field.length == 0 || defaultValues[field_id] == undefined) {
602         return;
603     }
604     setFieldValue(field, getFieldType(field), defaultValues[field_id]);
607 AJAX.registerOnload('config.js', function() {
608     $('div.tabs_contents')
609         .delegate('.restore-default, .set-value', 'mouseenter', function(){$(this).css('opacity', 1)})
610         .delegate('.restore-default, .set-value', 'mouseleave', function(){$(this).css('opacity', 0.25)})
611         .delegate('.restore-default, .set-value', 'click', function(e) {
612             e.preventDefault();
613             var href = $(this).attr('href');
614             var field_sel;
615             if ($(this).hasClass('restore-default')) {
616                 field_sel = href;
617                 restoreField(field_sel.substr(1));
618             } else {
619                 field_sel = href.match(/^[^=]+/)[0];
620                 var value = href.match(/=(.+)$/)[1];
621                 setFieldValue($(field_sel), 'text', value);
622             }
623             $(field_sel).trigger('change');
624         })
625         .find('.restore-default, .set-value')
626         // inline-block for IE so opacity inheritance works
627         .css({display: 'inline-block', opacity: 0.25});
631 // END: "Restore default" and "set value" buttons
632 // ------------------------------------------------------------------
634 // ------------------------------------------------------------------
635 // User preferences import/export
638 AJAX.registerOnload('config.js', function() {
639     offerPrefsAutoimport();
640     var radios = $('#import_local_storage, #export_local_storage');
641     if (!radios.length) {
642         return;
643     }
645     // enable JavaScript dependent fields
646     radios
647         .prop('disabled', false)
648         .add('#export_text_file, #import_text_file')
649         .click(function(){
650             var enable_id = $(this).attr('id');
651             var disable_id = enable_id.match(/local_storage$/)
652                 ? enable_id.replace(/local_storage$/, 'text_file')
653                 : enable_id.replace(/text_file$/, 'local_storage');
654             $('#opts_'+disable_id).addClass('disabled').find('input').prop('disabled', true);
655             $('#opts_'+enable_id).removeClass('disabled').find('input').prop('disabled', false);
656         });
658     // detect localStorage state
659     var ls_supported = window.localStorage || false;
660     var ls_exists = ls_supported ? (window.localStorage['config'] || false) : false;
661     $('div.localStorage-'+(ls_supported ? 'un' : '')+'supported').hide();
662     $('div.localStorage-'+(ls_exists ? 'empty' : 'exists')).hide();
663     if (ls_exists) {
664         updatePrefsDate();
665     }
666     $('form.prefs-form').change(function(){
667         var form = $(this);
668         var disabled = false;
669         if (!ls_supported) {
670             disabled = form.find('input[type=radio][value$=local_storage]').prop('checked');
671         } else if (!ls_exists && form.attr('name') == 'prefs_import'
672                 && $('#import_local_storage')[0].checked) {
673             disabled = true;
674         }
675         form.find('input[type=submit]').prop('disabled', disabled);
676     }).submit(function(e) {
677         var form = $(this);
678         if (form.attr('name') == 'prefs_export' && $('#export_local_storage')[0].checked) {
679             e.preventDefault();
680             // use AJAX to read JSON settings and save them
681             savePrefsToLocalStorage(form);
682         } else if (form.attr('name') == 'prefs_import' && $('#import_local_storage')[0].checked) {
683             // set 'json' input and submit form
684             form.find('input[name=json]').val(window.localStorage['config']);
685         }
686     });
688     $('div.click-hide-message').live('click', function(){
689         $(this)
690         .hide()
691         .parent('.group')
692         .css('height', '')
693         .next('form')
694         .show();
695     });
699  * Saves user preferences to localStorage
701  * @param {Element} form
702  */
703 function savePrefsToLocalStorage(form)
705     form = $(form);
706     var submit = form.find('input[type=submit]');
707     submit.prop('disabled', true);
708     $.ajax({
709         url: 'prefs_manage.php',
710         cache: false,
711         type: 'POST',
712         data: {
713             ajax_request: true,
714             token: form.find('input[name=token]').val(),
715             submit_get_json: true
716         },
717         success: function(response) {
718             window.localStorage['config'] = response.prefs;
719             window.localStorage['config_mtime'] = response.mtime;
720             window.localStorage['config_mtime_local'] = (new Date()).toUTCString();
721             updatePrefsDate();
722             $('div.localStorage-empty').hide();
723             $('div.localStorage-exists').show();
724             var group = form.parent('.group');
725             group.css('height', group.height() + 'px');
726             form.hide('fast');
727             form.prev('.click-hide-message').show('fast');
728         },
729         complete: function() {
730             submit.prop('disabled', false);
731         }
732     });
736  * Updates preferences timestamp in Import form
737  */
738 function updatePrefsDate()
740     var d = new Date(window.localStorage['config_mtime_local']);
741     var msg = PMA_messages.strSavedOn.replace(
742         '@DATE@',
743         PMA_formatDateTime(d)
744     );
745     $('#opts_import_local_storage div.localStorage-exists').html(msg);
749  * Prepares message which informs that localStorage preferences are available and can be imported
750  */
751 function offerPrefsAutoimport()
753     var has_config = (window.localStorage || false) && (window.localStorage['config'] || false);
754     var cnt = $('#prefs_autoload');
755     if (!cnt.length || !has_config) {
756         return;
757     }
758     cnt.find('a').click(function(e) {
759         e.preventDefault();
760         var a = $(this);
761         if (a.attr('href') == '#no') {
762             cnt.remove();
763             $.post('index.php', {
764                 token: cnt.find('input[name=token]').val(),
765                 prefs_autoload: 'hide'});
766             return;
767         }
768         cnt.find('input[name=json]').val(window.localStorage['config']);
769         cnt.find('form').submit();
770     });
771     cnt.show();
775 // END: User preferences import/export
776 // ------------------------------------------------------------------