1 /* vim: set expandtab sw=4 ts=4 sts=4: */
3 * Functions used in configuration forms and on user preferences pages
6 // default values for fields
7 var defaultValues = {};
10 var PMA_messages = {};
15 * @param {Element} field
17 function getFieldType(field) {
19 var tagName = field.attr('tagName');
20 if (tagName == 'INPUT') {
21 return field.attr('type');
22 } else if (tagName == 'SELECT') {
24 } else if (tagName == 'TEXTAREA') {
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'
39 * @param {Element} field
40 * @param {String} field_type see {@link #getFieldType}
41 * @param {String|Boolean} [value]
43 function setFieldValue(field, field_type, value) {
47 field.attr('value', (value != undefined ? value : field.attr('defaultValue')));
50 field.attr('checked', (value != undefined ? value : field.attr('defaultChecked')));
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;
60 for (i = 0; i < imax; i++) {
61 options[i].selected = (value.indexOf(options[i].value) != -1);
73 * o String - if type is 'text'
74 * o boolean - if type is 'checkbox'
75 * o Array of values - if type is 'select'
77 * @param {Element} field
78 * @param {String} field_type returned by {@link #getFieldType}
79 * @type Boolean|String|String[]
81 function getFieldValue(field, field_type) {
85 return field.attr('value');
87 return field.attr('checked');
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);
102 * Returns values for all fields in fieldsets
104 function getAllValues() {
105 var elements = $('fieldset input, fieldset select, fieldset textarea');
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') {
116 values[elements[i].name] = value;
123 * Checks whether field has its default value
125 * @param {Element} field
126 * @param {String} type
129 function checkFieldDefault(field, type) {
131 var field_id = field.attr('id');
132 if (typeof defaultValues[field_id] == 'undefined') {
135 var isDefault = true;
136 var currentValue = getFieldValue(field, type);
137 if (type != 'select') {
138 isDefault = currentValue == defaultValues[field_id];
140 // compare arrays, will work for our representation of select values
141 if (currentValue.length != defaultValues[field_id].length) {
145 for (var i = 0; i < currentValue.length; i++) {
146 if (currentValue[i] != defaultValues[field_id][i]) {
157 * Returns element's id prefix
158 * @param {Element} element
160 function getIdPrefix(element) {
161 return $(element).attr('id').replace(/[^-]+$/, '');
164 // ------------------------------------------------------------------
165 // Form validation and field operations
168 // form validator assignments
171 // form validator list
173 // regexp: numeric value
174 _regexp_numeric: /^[0-9]+$/,
175 // regexp: extract parts from PCRE expression
176 _regexp_pcre_extract: /(.)(.*)\1(.*)?/,
178 * Validates positive number
180 * @param {boolean} isKeyUp
182 validate_positive_number: function (isKeyUp) {
183 if (isKeyUp && this.value == '') {
186 var result = this.value != '0' && validators._regexp_numeric.test(this.value);
187 return result ? true : PMA_messages['error_nan_p'];
190 * Validates non-negative number
192 * @param {boolean} isKeyUp
194 validate_non_negative_number: function (isKeyUp) {
195 if (isKeyUp && this.value == '') {
198 var result = validators._regexp_numeric.test(this.value);
199 return result ? true : PMA_messages['error_nan_nneg'];
202 * Validates port number
204 * @param {boolean} isKeyUp
206 validate_port_number: function(isKeyUp) {
207 if (this.value == '') {
210 var result = validators._regexp_numeric.test(this.value) && this.value != '0';
211 return result && this.value <= 65535 ? true : PMA_messages['error_incorrect_port'];
214 * Validates value according to given regular expression
216 * @param {boolean} isKeyUp
217 * @param {string} regexp
219 validate_by_regex: function(isKeyUp, regexp) {
220 if (isKeyUp && this.value == '') {
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'];
229 * Validates upper bound for numeric inputs
231 * @param {boolean} isKeyUp
232 * @param {int} max_value
234 validate_upper_bound: function(isKeyUp, max_value) {
235 var val = parseInt(this.value);
239 return val <= max_value ? true : PMA_messages['error_value_lte'].replace('%s', max_value);
244 // fieldset validators
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
257 function validateField(id, type, onKeyUp, params) {
258 if (typeof validators[type] == 'undefined') {
261 if (typeof validate[id] == 'undefined') {
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
273 * @return array of [function, paramseters to be passed to function]
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]];
282 // look for registered validators
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]) {
290 functions.push([validators[validate[field_id][i][0]], validate[field_id][i][1]]);
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}
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) {
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');
327 // if error container doesn't exist, create it
328 if (errorCnt.length == 0) {
330 errorCnt = $('<dl class="errors" />');
331 field.find('table').before(errorCnt);
333 errorCnt = $('<dl class="inline_errors" />');
334 field.closest('td').append(errorCnt);
339 for (var i = 0, imax = errors.length; i < imax; i++) {
340 html += '<dd>' + errors[i] + '</dd>';
343 } else if (errorCnt !== null) {
344 // remove useless error container
351 * Validates fieldset and puts errors in 'errors' object
353 * @param {Element} fieldset
354 * @param {boolean} isKeyUp
355 * @param {Object} errors
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] = [];
365 if (typeof fieldset_errors[field_id] == 'string') {
366 fieldset_errors[field_id] = [fieldset_errors[field_id]];
368 $.merge(errors[field_id], fieldset_errors[field_id]);
374 * Validates form field and puts errors in 'errors' object
376 * @param {Element} field
377 * @param {boolean} isKeyUp
378 * @param {Object} errors
380 function validate_field(field, isKeyUp, errors) {
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)
389 args.unshift(isKeyUp);
390 var result = functions[i][0].apply(field[0], args);
391 if (result !== true) {
392 if (typeof result == 'string') {
395 $.merge(errors[field_id], result);
401 * Validates form field and parent fieldset
403 * @param {Element} field
404 * @param {boolean} isKeyUp
406 function validate_field_and_fieldset(field, isKeyUp) {
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
419 function markField(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
436 function setRestoreDefaultBtn(field, display) {
437 var el = $(field).closest('td').find('.restore-default img');
438 el[display ? 'show' : 'hide']();
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(){
447 el.bind('change', function() {
448 validate_field_and_fieldset(this, false);
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);
459 // disable textarea spellcheck
460 if (tagName == 'TEXTAREA') {
461 el.attr('spellcheck', false);
465 // check whether we've refreshed a page and browser remembered modified
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
471 for (var i = 0; i < elements.length; i++) {
472 validate_field(elements[i], false, errors);
474 // run all fieldset validators
475 $('fieldset').each(function(){
476 validate_fieldset(this, false, errors);
479 displayErrors(errors);
480 } else if (check_page_refresh) {
481 check_page_refresh.val('1');
486 // END: Form validation and field operations
487 // ------------------------------------------------------------------
489 // ------------------------------------------------------------------
496 * @param {String} tab_id
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);
506 var tabs = $('.tabs');
510 // add tabs events and activate one tab (the first one or indicated by location hash)
514 setTab($(this).attr('href'));
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)
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));
532 setInterval(tab_check_fnc, 200);
537 // ------------------------------------------------------------------
539 // ------------------------------------------------------------------
540 // Form reset buttons
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]));
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
565 function restoreField(field_id) {
566 var field = $('#'+field_id);
567 if (field.length == 0 || defaultValues[field_id] == undefined) {
570 setFieldValue(field, getFieldType(field), defaultValues[field_id]);
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) {
579 var href = $(this).attr('href');
581 if ($(this).hasClass('restore-default')) {
583 restoreField(field_sel.substr(1));
585 field_sel = href.match(/^[^=]+/)[0];
586 var value = href.match(/=(.+)$/)[1];
587 setFieldValue($(field_sel), 'text', value);
589 $(field_sel).trigger('change');
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
605 offerPrefsAutoimport();
606 var radios = $('#import_local_storage, #export_local_storage');
607 if (!radios.length) {
611 // enable JavaScript dependent fields
613 .attr('disabled', false)
614 .add('#export_text_file, #import_text_file')
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);
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();
632 $('form.prefs-form').change(function(){
634 var disabled = false;
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) {
641 form.find('input[type=submit]').attr('disabled', disabled);
642 }).submit(function(e) {
644 if (form.attr('name') == 'prefs_export' && $('#export_local_storage')[0].checked) {
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']);
654 $('.click-hide-message').live('click', function(){
656 div.hide().parent('.group').css('height', '');
657 div.next('form').show();
662 * Saves user preferences to localStorage
664 * @param {Element} form
666 function savePrefsToLocalStorage(form)
669 var submit = form.find('input[type=submit]');
670 submit.attr('disabled', true);
672 url: 'prefs_manage.php',
676 token: form.find('input[name=token]').val(),
677 submit_get_json: true
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();
684 $('.localStorage-empty').hide();
685 $('.localStorage-exists').show();
686 var group = form.parent('.group');
687 group.css('height', group.height() + 'px');
689 form.prev('.click-hide-message').show('fast');
691 complete: function() {
692 submit.attr('disabled', false);
698 * Updates preferences timestamp in Import form
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
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
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) {
731 cnt.find('a').click(function(e) {
734 if (a.attr('href') == '#no') {
737 token: cnt.find('input[name=token]').val(),
738 prefs_autoload: 'hide'});
741 cnt.find('input[name=json]').val(window.localStorage['config']);
742 cnt.find('form').submit();
748 // END: User preferences import/export
749 // ------------------------------------------------------------------