/* global isStorageSupported */ // js/config.js /* global ChartType, ColumnType, DataTable, JQPlotChartFactory */ // js/chart.js /* global DatabaseStructure */ // js/database/structure.js /* global mysqlDocBuiltin, mysqlDocKeyword */ // js/doclinks.js /* global Indexes */ // js/indexes.js /* global firstDayOfCalendar, maxInputVars, mysqlDocTemplate, themeImagePath */ // templates/javascript/variables.twig /* global sprintf */ // js/vendor/sprintf.js /* global zxcvbnts */ // js/vendor/zxcvbn-ts.js /** * general function, usually for data manipulation pages * @test-module Functions */ var Functions = {}; /** * @var sqlBoxLocked lock for the sqlbox textarea in the querybox */ // eslint-disable-next-line no-unused-vars var sqlBoxLocked = false; /** * @var {array}, holds elements which content should only selected once */ var onlyOnceElements = []; /** * @var {number} ajaxMessageCount Number of AJAX messages shown since page load */ var ajaxMessageCount = 0; /** * @var codeMirrorEditor object containing CodeMirror editor of the query editor in SQL tab */ var codeMirrorEditor = false; /** * @var codeMirrorInlineEditor object containing CodeMirror editor of the inline query editor */ var codeMirrorInlineEditor = false; /** * @var {boolean} sqlAutoCompleteInProgress shows if Table/Column name autocomplete AJAX is in progress */ var sqlAutoCompleteInProgress = false; /** * @var sqlAutoComplete object containing list of columns in each table */ var sqlAutoComplete = false; /** * @var {string} sqlAutoCompleteDefaultTable string containing default table to autocomplete columns */ var sqlAutoCompleteDefaultTable = ''; /** * @var {array} centralColumnList array to hold the columns in central list per db. */ var centralColumnList = []; /** * @var {array} primaryIndexes array to hold 'Primary' index columns. */ // eslint-disable-next-line no-unused-vars var primaryIndexes = []; /** * @var {array} uniqueIndexes array to hold 'Unique' index columns. */ // eslint-disable-next-line no-unused-vars var uniqueIndexes = []; /** * @var {array} indexes array to hold 'Index' columns. */ // eslint-disable-next-line no-unused-vars var indexes = []; /** * @var {array} fulltextIndexes array to hold 'Fulltext' columns. */ // eslint-disable-next-line no-unused-vars var fulltextIndexes = []; /** * @var {array} spatialIndexes array to hold 'Spatial' columns. */ // eslint-disable-next-line no-unused-vars var spatialIndexes = []; /** * Make sure that ajax requests will not be cached * by appending a random variable to their parameters */ $.ajaxPrefilter(function (options, originalOptions) { var nocache = new Date().getTime() + '' + Math.floor(Math.random() * 1000000); if (typeof options.data === 'string') { options.data += '&_nocache=' + nocache + '&token=' + encodeURIComponent(CommonParams.get('token')); } else if (typeof options.data === 'object') { options.data = $.extend(originalOptions.data, { '_nocache' : nocache, 'token': CommonParams.get('token') }); } }); /** * Adds a date/time picker to an element * * @param {object} $thisElement a jQuery object pointing to the element * @param {string} type * @param {object} options */ Functions.addDatepicker = function ($thisElement, type, options) { if (type !== 'date' && type !== 'time' && type !== 'datetime' && type !== 'timestamp') { return; } var showTimepicker = true; if (type === 'date') { showTimepicker = false; } // Getting the current Date and time var currentDateTime = new Date(); var defaultOptions = { timeInput : true, hour: currentDateTime.getHours(), minute: currentDateTime.getMinutes(), second: currentDateTime.getSeconds(), showOn: 'button', buttonImage: themeImagePath + 'b_calendar.png', buttonImageOnly: true, stepMinutes: 1, stepHours: 1, showSecond: true, showMillisec: true, showMicrosec: true, showTimepicker: showTimepicker, showButtonPanel: false, changeYear: true, dateFormat: 'yy-mm-dd', // yy means year with four digits timeFormat: 'HH:mm:ss.lc', constrainInput: false, altFieldTimeOnly: false, showAnim: '', beforeShow: function (input, inst) { // Remember that we came from the datepicker; this is used // in table/change.js by verificationsAfterFieldChange() $thisElement.data('comes_from', 'datepicker'); if ($(input).closest('.cEdit').length > 0) { setTimeout(function () { inst.dpDiv.css({ top: 0, left: 0, position: 'relative' }); }, 0); } setTimeout(function () { // Fix wrong timepicker z-index, doesn't work without timeout $('#ui-timepicker-div').css('z-index', $('#ui-datepicker-div').css('z-index')); // Integrate tooltip text into dialog var tooltip = $thisElement.uiTooltip('instance'); if (typeof tooltip !== 'undefined') { tooltip.disable(); var $note = $('
');
$note.text(tooltip.option('content'));
$('div.ui-datepicker').append($note);
}
}, 0);
},
onSelect: function () {
$thisElement.data('datepicker').inline = true;
},
onClose: function () {
// The value is no more from the date picker
$thisElement.data('comes_from', '');
if (typeof $thisElement.data('datepicker') !== 'undefined') {
$thisElement.data('datepicker').inline = false;
}
var tooltip = $thisElement.uiTooltip('instance');
if (typeof tooltip !== 'undefined') {
tooltip.enable();
}
}
};
if (type === 'time') {
$thisElement.timepicker($.extend(defaultOptions, options));
// Add a tip regarding entering MySQL allowed-values for TIME data-type
Functions.tooltip($thisElement, 'input', Messages.strMysqlAllowedValuesTipTime);
} else {
$thisElement.datetimepicker($.extend(defaultOptions, options));
}
};
/**
* Add a date/time picker to each element that needs it
* (only when jquery-ui-timepicker-addon.js is loaded)
*/
Functions.addDateTimePicker = function () {
if ($.timepicker !== undefined) {
$('input.timefield, input.datefield, input.datetimefield').each(function () {
var decimals = $(this).parent().attr('data-decimals');
var type = $(this).parent().attr('data-type');
var showMillisec = false;
var showMicrosec = false;
var timeFormat = 'HH:mm:ss';
var hourMax = 23;
// check for decimal places of seconds
if (decimals > 0 && type.indexOf('time') !== -1) {
if (decimals > 3) {
showMillisec = true;
showMicrosec = true;
timeFormat = 'HH:mm:ss.lc';
} else {
showMillisec = true;
timeFormat = 'HH:mm:ss.l';
}
}
if (type === 'time') {
hourMax = 99;
}
Functions.addDatepicker($(this), type, {
showMillisec: showMillisec,
showMicrosec: showMicrosec,
timeFormat: timeFormat,
hourMax: hourMax,
firstDay: firstDayOfCalendar
});
// Add a tip regarding entering MySQL allowed-values
// for TIME and DATE data-type
if ($(this).hasClass('timefield')) {
Functions.tooltip($(this), 'input', Messages.strMysqlAllowedValuesTipTime);
} else if ($(this).hasClass('datefield')) {
Functions.tooltip($(this), 'input', Messages.strMysqlAllowedValuesTipDate);
}
});
}
};
/**
* Handle redirect and reload flags sent as part of AJAX requests
*
* @param data ajax response data
*/
Functions.handleRedirectAndReload = function (data) {
if (parseInt(data.redirect_flag) === 1) {
// add one more GET param to display session expiry msg
if (window.location.href.indexOf('?') === -1) {
window.location.href += '?session_expired=1';
} else {
window.location.href += CommonParams.get('arg_separator') + 'session_expired=1';
}
window.location.reload();
} else if (parseInt(data.reload_flag) === 1) {
window.location.reload();
}
};
/**
* Creates an SQL editor which supports auto completing etc.
*
* @param $textarea jQuery object wrapping the textarea to be made the editor
* @param options optional options for CodeMirror
* @param {'vertical'|'horizontal'|'both'} resize optional resizing ('vertical', 'horizontal', 'both')
* @param lintOptions additional options for lint
*
* @return {object|null}
*/
Functions.getSqlEditor = function ($textarea, options, resize, lintOptions) {
var resizeType = resize;
if ($textarea.length > 0 && typeof CodeMirror !== 'undefined') {
// merge options for CodeMirror
var defaults = {
lineNumbers: true,
matchBrackets: true,
extraKeys: { 'Ctrl-Space': 'autocomplete' },
hintOptions: { 'completeSingle': false, 'completeOnSingleClick': true },
indentUnit: 4,
mode: 'text/x-mysql',
lineWrapping: true
};
if (CodeMirror.sqlLint) {
$.extend(defaults, {
gutters: ['CodeMirror-lint-markers'],
lint: {
'getAnnotations': CodeMirror.sqlLint,
'async': true,
'lintOptions': lintOptions
}
});
}
$.extend(true, defaults, options);
// create CodeMirror editor
var codemirrorEditor = CodeMirror.fromTextArea($textarea[0], defaults);
// allow resizing
if (! resizeType) {
resizeType = 'vertical';
}
var handles = '';
if (resizeType === 'vertical') {
handles = 's';
}
if (resizeType === 'both') {
handles = 'all';
}
if (resizeType === 'horizontal') {
handles = 'e, w';
}
$(codemirrorEditor.getWrapperElement())
.css('resize', resizeType)
.resizable({
handles: handles,
resize: function () {
codemirrorEditor.setSize($(this).width(), $(this).height());
}
});
// enable autocomplete
codemirrorEditor.on('inputRead', Functions.codeMirrorAutoCompleteOnInputRead);
// page locking
codemirrorEditor.on('change', function (e) {
e.data = {
value: 3,
content: codemirrorEditor.isClean(),
};
AJAX.lockPageHandler(e);
});
return codemirrorEditor;
}
return null;
};
/**
* Clear text selection
*/
Functions.clearSelection = function () {
if (document.selection && document.selection.empty) {
document.selection.empty();
} else if (window.getSelection) {
var sel = window.getSelection();
if (sel.empty) {
sel.empty();
}
if (sel.removeAllRanges) {
sel.removeAllRanges();
}
}
};
/**
* Create a jQuery UI tooltip
*
* @param $elements jQuery object representing the elements
* @param item the item
* (see https://api.jqueryui.com/tooltip/#option-items)
* @param myContent content of the tooltip
* @param additionalOptions to override the default options
*
*/
Functions.tooltip = function ($elements, item, myContent, additionalOptions) {
if ($('#no_hint').length > 0) {
return;
}
var defaultOptions = {
content: myContent,
items: item,
tooltipClass: 'tooltip',
track: true,
show: false,
hide: false
};
$elements.uiTooltip($.extend(true, defaultOptions, additionalOptions));
};
/**
* HTML escaping
*
* @param {any} unsafe
* @return {string | false}
*/
Functions.escapeHtml = function (unsafe) {
if (typeof(unsafe) !== 'undefined') {
return unsafe
.toString()
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
} else {
return false;
}
};
/**
* JavaScript escaping
*
* @param {any} unsafe
* @return {string | false}
*/
Functions.escapeJsString = function (unsafe) {
if (typeof(unsafe) !== 'undefined') {
return unsafe
.toString()
.replace('\x00', '')
.replace('\\', '\\\\')
.replace('\'', '\\\'')
.replace(''', '\\'')
.replace('"', '\\"')
.replace('"', '\\"')
.replace('\n', '\n')
.replace('\r', '\r')
.replace(/<\/script/gi, '\' + \'script');
} else {
return false;
}
};
/**
* @param {string} s
* @return {string}
*/
Functions.escapeBacktick = function (s) {
return s.replace('`', '``');
};
/**
* @param {string} s
* @return {string}
*/
Functions.escapeSingleQuote = function (s) {
return s.replace('\\', '\\\\').replace('\'', '\\\'');
};
Functions.sprintf = function () {
return sprintf.apply(this, arguments);
};
/**
* Hides/shows the default value input field, depending on the default type
* Ticks the NULL checkbox if NULL is chosen as default value.
*
* @param {JQuery').addClass('align-middle');
$(' ').html(Messages.strGeneratePassword).appendTo(generatePwdRow);
var pwdCell = $(' ').appendTo(generatePwdRow);
var pwdButton = $('')
.attr({ type: 'button', id: 'button_generate_password', value: Messages.strGenerate })
.addClass('btn btn-secondary button')
.on('click', function () {
Functions.suggestPassword(this.form);
});
var pwdTextbox = $('')
.attr({ type: 'text', name: 'generated_pw', id: 'generated_pw' });
pwdCell.append(pwdButton).append(pwdTextbox);
if (document.getElementById('button_generate_password') === null) {
$('#tr_element_before_generate_password').parent().append(generatePwdRow);
}
var generatePwdDiv = $('').addClass('item');
$('').attr({ for: 'button_generate_password' })
.html(Messages.strGeneratePassword + ':')
.appendTo(generatePwdDiv);
var optionsSpan = $('').addClass('options')
.appendTo(generatePwdDiv);
pwdButton.clone(true).appendTo(optionsSpan);
pwdTextbox.clone(true).appendTo(generatePwdDiv);
if (document.getElementById('button_generate_password') === null) {
$('#div_element_before_generate_password').parent().append(generatePwdDiv);
}
};
/**
* selects the content of a given object, f.e. a textarea
*
* @param {object} element element of which the content will be selected
* @param {any | true} lock variable which holds the lock for this element or true, if no lock exists
* @param {boolean} onlyOnce boolean if true this is only done once f.e. only on first focus
*/
Functions.selectContent = function (element, lock, onlyOnce) {
if (onlyOnce && onlyOnceElements[element.name]) {
return;
}
onlyOnceElements[element.name] = true;
if (lock) {
return;
}
element.select();
};
/**
* Displays a confirmation box before submitting a "DROP/DELETE/ALTER" query.
* This function is called while clicking links
*
* @param {object} theLink the link
* @param {object} theSqlQuery the sql query to submit
*
* @return {boolean} whether to run the query or not
*/
Functions.confirmLink = function (theLink, theSqlQuery) {
// Confirmation is not required in the configuration file
// or browser is Opera (crappy js implementation)
if (Messages.strDoYouReally === '' || typeof(window.opera) !== 'undefined') {
return true;
}
var isConfirmed = confirm(Functions.sprintf(Messages.strDoYouReally, theSqlQuery));
if (isConfirmed) {
if (typeof(theLink.href) !== 'undefined') {
theLink.href += CommonParams.get('arg_separator') + 'is_js_confirmed=1';
} else if (typeof(theLink.form) !== 'undefined') {
theLink.form.action += '?is_js_confirmed=1';
}
}
return isConfirmed;
};
/**
* Confirms a "DROP/DELETE/ALTER" query before
* submitting it if required.
* This function is called by the 'Functions.checkSqlQuery()' js function.
*
* @param {object} theForm1 the form
* @param {string} sqlQuery1 the sql query string
*
* @return {boolean} whether to run the query or not
*
* @see Functions.checkSqlQuery()
*/
Functions.confirmQuery = function (theForm1, sqlQuery1) {
// Confirmation is not required in the configuration file
if (Messages.strDoYouReally === '') {
return true;
}
// Confirms a "DROP/DELETE/ALTER/TRUNCATE" statement
//
// TODO: find a way (if possible) to use the parser-analyser
// for this kind of verification
// For now, I just added a ^ to check for the statement at
// beginning of expression
var doConfirmRegExp0 = new RegExp('^\\s*DROP\\s+(IF EXISTS\\s+)?(TABLE|PROCEDURE)\\s', 'i');
var doConfirmRegExp1 = new RegExp('^\\s*ALTER\\s+TABLE\\s+((`[^`]+`)|([A-Za-z0-9_$]+))\\s+DROP\\s', 'i');
var doConfirmRegExp2 = new RegExp('^\\s*DELETE\\s+FROM\\s', 'i');
var doConfirmRegExp3 = new RegExp('^\\s*TRUNCATE\\s', 'i');
var doConfirmRegExp4 = new RegExp('^(?=.*UPDATE\\b)^((?!WHERE).)*$', 'i');
if (doConfirmRegExp0.test(sqlQuery1) ||
doConfirmRegExp1.test(sqlQuery1) ||
doConfirmRegExp2.test(sqlQuery1) ||
doConfirmRegExp3.test(sqlQuery1) ||
doConfirmRegExp4.test(sqlQuery1)) {
var message;
if (sqlQuery1.length > 100) {
message = sqlQuery1.substr(0, 100) + '\n ...';
} else {
message = sqlQuery1;
}
var isConfirmed = confirm(Functions.sprintf(Messages.strDoYouReally, message));
// statement is confirmed -> update the
// "is_js_confirmed" form field so the confirm test won't be
// run on the server side and allows to submit the form
if (isConfirmed) {
theForm1.elements.is_js_confirmed.value = 1;
return true;
} else {
// statement is rejected -> do not submit the form
window.focus();
return false;
} // end if (handle confirm box result)
} // end if (display confirm box)
return true;
};
/**
* Displays an error message if the user submitted the sql query form with no
* sql query, else checks for "DROP/DELETE/ALTER" statements
*
* @param {object} theForm the form
*
* @return {boolean} always false
*
* @see Functions.confirmQuery()
*/
Functions.checkSqlQuery = function (theForm) {
// get the textarea element containing the query
var sqlQuery;
if (codeMirrorEditor) {
codeMirrorEditor.save();
sqlQuery = codeMirrorEditor.getValue();
} else {
sqlQuery = theForm.elements.sql_query.value;
}
var spaceRegExp = new RegExp('\\s+');
if (typeof(theForm.elements.sql_file) !== 'undefined' &&
theForm.elements.sql_file.value.replace(spaceRegExp, '') !== '') {
return true;
}
if (typeof(theForm.elements.id_bookmark) !== 'undefined' &&
(theForm.elements.id_bookmark.value !== null || theForm.elements.id_bookmark.value !== '') &&
theForm.elements.id_bookmark.selectedIndex !== 0) {
return true;
}
var result = false;
// Checks for "DROP/DELETE/ALTER" statements
if (sqlQuery.replace(spaceRegExp, '') !== '') {
result = Functions.confirmQuery(theForm, sqlQuery);
} else {
alert(Messages.strFormEmpty);
}
if (codeMirrorEditor) {
codeMirrorEditor.focus();
} else if (codeMirrorInlineEditor) {
codeMirrorInlineEditor.focus();
}
return result;
};
/**
* Check if a form's element is empty.
* An element containing only spaces is also considered empty
*
* @param {object} theForm the form
* @param {string} theFieldName the name of the form field to put the focus on
*
* @return {boolean} whether the form field is empty or not
*/
Functions.emptyCheckTheField = function (theForm, theFieldName) {
var theField = theForm.elements[theFieldName];
var spaceRegExp = new RegExp('\\s+');
return theField.value.replace(spaceRegExp, '') === '';
};
/**
* Ensures a value submitted in a form is numeric and is in a range
*
* @param {object} theForm the form
* @param {string} theFieldName the name of the form field to check
* @param {any} message
* @param {number} minimum the minimum authorized value
* @param {number} maximum the maximum authorized value
*
* @return {boolean} whether a valid number has been submitted or not
*/
Functions.checkFormElementInRange = function (theForm, theFieldName, message, minimum, maximum) {
var theField = theForm.elements[theFieldName];
var val = parseInt(theField.value, 10);
var min = 0;
var max = Number.MAX_VALUE;
if (typeof(minimum) !== 'undefined') {
min = minimum;
}
if (typeof(maximum) !== 'undefined' && maximum !== null) {
max = maximum;
}
if (isNaN(val)) {
theField.select();
alert(Messages.strEnterValidNumber);
theField.focus();
return false;
} else if (val < min || val > max) {
theField.select();
alert(Functions.sprintf(message, val));
theField.focus();
return false;
} else {
theField.value = val;
}
return true;
};
Functions.checkTableEditForm = function (theForm, fieldsCnt) {
// TODO: avoid sending a message if user just wants to add a line
// on the form but has not completed at least one field name
var atLeastOneField = 0;
var i;
var elm;
var elm2;
var elm3;
var val;
var id;
for (i = 0; i < fieldsCnt; i++) {
id = '#field_' + i + '_2';
elm = $(id);
val = elm.val();
if (val === 'VARCHAR' || val === 'CHAR' || val === 'BIT' || val === 'VARBINARY' || val === 'BINARY') {
elm2 = $('#field_' + i + '_3');
val = parseInt(elm2.val(), 10);
elm3 = $('#field_' + i + '_1');
if (isNaN(val) && elm3.val() !== '') {
elm2.select();
alert(Messages.strEnterValidLength);
elm2.focus();
return false;
}
}
if (atLeastOneField === 0) {
id = 'field_' + i + '_1';
if (!Functions.emptyCheckTheField(theForm, id)) {
atLeastOneField = 1;
}
}
}
if (atLeastOneField === 0) {
var theField = theForm.elements.field_0_1;
alert(Messages.strFormEmpty);
theField.focus();
return false;
}
// at least this section is under jQuery
var $input = $('input.textfield[name=\'table\']');
if ($input.val() === '') {
alert(Messages.strFormEmpty);
$input.trigger('focus');
return false;
}
return true;
};
/**
* True if last click is to check a row.
*/
var lastClickChecked = false;
/**
* Zero-based index of last clicked row.
* Used to handle the shift + click event in the code above.
*/
var lastClickedRow = -1;
/**
* Zero-based index of last shift clicked row.
*/
var lastShiftClickedRow = -1;
var idleSecondsCounter = 0;
var incInterval;
var updateTimeout;
AJAX.registerTeardown('functions.js', function () {
clearTimeout(updateTimeout);
clearInterval(incInterval);
$(document).off('mousemove');
});
AJAX.registerOnload('functions.js', function () {
document.onclick = function () {
idleSecondsCounter = 0;
};
$(document).on('mousemove',function () {
idleSecondsCounter = 0;
});
document.onkeypress = function () {
idleSecondsCounter = 0;
};
function guid () {
function s4 () {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
function SetIdleTime () {
idleSecondsCounter++;
}
function UpdateIdleTime () {
var href = 'index.php?route=/';
var guid = 'default';
if (isStorageSupported('sessionStorage')) {
guid = window.sessionStorage.guid;
}
var params = {
'ajax_request' : true,
'server' : CommonParams.get('server'),
'db' : CommonParams.get('db'),
'guid': guid,
'access_time': idleSecondsCounter,
'check_timeout': 1
};
$.ajax({
type: 'POST',
url: href,
data: params,
success: function (data) {
if (data.success) {
if (CommonParams.get('LoginCookieValidity') - idleSecondsCounter < 0) {
/* There is other active window, let's reset counter */
idleSecondsCounter = 0;
}
var remaining = Math.min(
/* Remaining login validity */
CommonParams.get('LoginCookieValidity') - idleSecondsCounter,
/* Remaining time till session GC */
CommonParams.get('session_gc_maxlifetime')
);
var interval = 1000;
if (remaining > 5) {
// max value for setInterval() function
interval = Math.min((remaining - 1) * 1000, Math.pow(2, 31) - 1);
}
updateTimeout = window.setTimeout(UpdateIdleTime, interval);
} else { // timeout occurred
clearInterval(incInterval);
if (isStorageSupported('sessionStorage')) {
window.sessionStorage.clear();
}
// append the login form on the page, disable all the forms which were not disabled already, close all the open jqueryui modal boxes
if (!$('#modalOverlay').length) {
$('fieldset').not(':disabled').attr('disabled', 'disabled').addClass('disabled_for_expiration');
$('body').append(data.error);
$('.ui-dialog').each(function () {
$('#' + $(this).attr('aria-describedby')).dialog('close');
});
$('#input_username').trigger('focus');
} else {
CommonParams.set('token', data.new_token);
$('input[name=token]').val(data.new_token);
}
idleSecondsCounter = 0;
Functions.handleRedirectAndReload(data);
}
}
});
}
if (CommonParams.get('logged_in')) {
incInterval = window.setInterval(SetIdleTime, 1000);
var sessionTimeout = Math.min(
CommonParams.get('LoginCookieValidity'),
CommonParams.get('session_gc_maxlifetime')
);
if (isStorageSupported('sessionStorage')) {
window.sessionStorage.setItem('guid', guid());
}
var interval = (sessionTimeout - 5) * 1000;
if (interval > Math.pow(2, 31) - 1) { // max value for setInterval() function
interval = Math.pow(2, 31) - 1;
}
updateTimeout = window.setTimeout(UpdateIdleTime, interval);
}
});
/**
* Unbind all event handlers before tearing down a page
*/
AJAX.registerTeardown('functions.js', function () {
$(document).off('click', 'input:checkbox.checkall');
});
AJAX.registerOnload('functions.js', function () {
/**
* Row marking in horizontal mode (use "on" so that it works also for
* next pages reached via AJAX); a tr may have the class noclick to remove
* this behavior.
*/
$(document).on('click', 'input:checkbox.checkall', function (e) {
var $this = $(this);
var $tr = $this.closest('tr');
var $table = $this.closest('table');
if (!e.shiftKey || lastClickedRow === -1) {
// usual click
var $checkbox = $tr.find(':checkbox.checkall');
var checked = $this.prop('checked');
$checkbox.prop('checked', checked).trigger('change');
if (checked) {
$tr.addClass('marked table-active');
} else {
$tr.removeClass('marked table-active');
}
lastClickChecked = checked;
// remember the last clicked row
lastClickedRow = lastClickChecked ? $table.find('tbody tr:not(.noclick)').index($tr) : -1;
lastShiftClickedRow = -1;
} else {
// handle the shift click
Functions.clearSelection();
var start;
var end;
// clear last shift click result
if (lastShiftClickedRow >= 0) {
if (lastShiftClickedRow >= lastClickedRow) {
start = lastClickedRow;
end = lastShiftClickedRow;
} else {
start = lastShiftClickedRow;
end = lastClickedRow;
}
$tr.parent().find('tr:not(.noclick)')
.slice(start, end + 1)
.removeClass('marked table-active')
.find(':checkbox')
.prop('checked', false)
.trigger('change');
}
// handle new shift click
var currRow = $table.find('tbody tr:not(.noclick)').index($tr);
if (currRow >= lastClickedRow) {
start = lastClickedRow;
end = currRow;
} else {
start = currRow;
end = lastClickedRow;
}
$tr.parent().find('tr:not(.noclick)')
.slice(start, end + 1)
.addClass('marked table-active')
.find(':checkbox')
.prop('checked', true)
.trigger('change');
// remember the last shift clicked row
lastShiftClickedRow = currRow;
}
});
Functions.addDateTimePicker();
/**
* Add attribute to text boxes for iOS devices (based on bugID: 3508912)
*/
if (navigator.userAgent.match(/(iphone|ipod|ipad)/i)) {
$('input[type=text]').attr('autocapitalize', 'off').attr('autocorrect', 'off');
}
});
/**
* Checks/unchecks all options of a