/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
const MESSAGE_LEVEL_NONE = 0;
const MESSAGE_LEVEL_INFO = 1;
const MESSAGE_LEVEL_WARN = 2;
const MESSAGE_LEVEL_ERROR = 3;
const MESSAGE_LEVEL_CSRF = 4;
const MESSAGE_LEVEL_MIXED = 5;
var theme;
var myRefresh;
var userMenuTimer;
var userMenuOpenTimer = null;
var graphMenuTimer;
var graphMenuElement = 0;
var callbackTimer = {};
var callbackClickTimer = {}
var callbackOpen = {};
var pulsating = true;
var pageLoaded = false;
var shiftPressed = false;
var sessionMessage = null;
var sessionMessageOpen = null;
var sessionMessageTimer = null;
var currentNavState = null;
var currentNavId = null;
var myTitle;
var myHref;
var lastPage = null;
var statePushed = false;
var popFired = false;
var hostInfoHeight = 0;
var marginLeftTree = null;
var marginLeftConsole = null;
var minTreeWidth = null;
var maxTreeWidth = null;
var pageName;
var columnsHidden = 0;
var lastColumnsHidden = {};
var lastWidth = {};
var resizeDelta = 100;
var resizeTime = 0;
var resizeTimeout = false;
var formArray = {};
var formRules = {};
var pageWidth = null;
var isHover = false;
var hoverTimer = false;
var previousMainWidth = null;
var previousColumns = null;
var pinnedTooltipElement = null;
var faIcons = {
open: {
icon: ''
},
close: {
icon: ''
},
checkAll: {
icon: ''
},
uncheckAll: {
icon: ''
},
flipAll: {
icon: ''
},
collapseAll: {
icon: ''
},
expandAll: {
icon: ''
},
collapse: {
icon: ''
},
expand: {
icon: ''
}
};
let select2Setup = {
displayDefaultLabel : false,
}
/* size the open dropdown to fit its options instead of the (possibly narrower) control */
if (typeof $ !== 'undefined' && $.fn.select2) {
$.fn.select2.defaults.set('dropdownAutoWidth', true);
}
window.paceOptions = {
ajax: true,
document: true,
elements: false,
minTime: 400,
startOnPageLoad: false,
restartOnPushState: false,
restartOnRequestAfter: 120,
eventLag: false,
};
window.onbeforeunload = renderLoading;
function renderLoading() {
Pace.stop();
Pace.bar.render();
setCactiTabCookie();
}
function clearPinnedTooltip() {
if (pinnedTooltipElement && pinnedTooltipElement.length) {
var target = pinnedTooltipElement;
pinnedTooltipElement = null;
target.removeData('cacti-tooltip-pinned');
if ($(document).tooltip('instance')) {
$(document).tooltip('close', $.Event('click', {
target: target[0],
currentTarget: target[0]
}));
}
}
}
var isMobile = {
Android: function () {
return navigator.userAgent.match(/Android/i);
},
BlackBerry: function () {
return navigator.userAgent.match(/BlackBerry/i);
},
iOS: function () {
return navigator.userAgent.match(/iPhone|iPad|iPod/i);
},
Opera: function () {
return navigator.userAgent.match(/Opera Mini/i);
},
Windows: function () {
return navigator.userAgent.match(/IEMobile/i);
},
any: function () {
return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Opera() || isMobile.Windows());
}
};
/* simple ajax request queueing */
jQuery.ajaxQ = (function () {
var id = 0, Q = {};
jQuery(document).ajaxSend(function (e, jqx) {
jqx._id = ++id;
Q[jqx._id] = jqx;
});
jQuery(document).ajaxComplete(function (e, jqx) {
delete Q[jqx._id];
});
return {
abortAll: function () {
var r = [];
jQuery.each(Q, function (i, jqx) {
r.push(jqx._id);
jqx.abort();
});
return r;
}
};
})();
/**
* basename - this function will return the basename
* of the php script called
*
* @param path - the document.url
* @param suffix - remove the named suffix from the file
*/
function basename(path, suffix) {
var b = path;
b.charAt(b.length - 1);
// Don't remove trailing slash for now //
/*
if (lastChar === '/' || lastChar === '\\\\') {
b = b.slice(0, -1);
}
*/
if (b.indexOf('?') > 0) {
var questionPosition = b.indexOf('?');
b = b.slice(0, questionPosition);
}
b = b.replace(/^.*[\\/\\\\]/g, '');
if (suffix !== undefined && b.substr(b.length - suffix.length) == suffix) {
b = b.substr(0, b.length - suffix.length);
}
if (b.length == 0) {
b = "index.php";
}
return b;
}
/** getTimestampFromDate - Simple function to convert a MySQL Date
* to a timestamp */
function getTimestampFromDate(dateStamp) {
if (dateStamp !== undefined) {
var dateParts = dateStamp.split(' ');
var timeParts = dateParts[1].split(':');
dateParts = dateParts[0].split('-');
var date = new Date(dateParts[0], Number.parseInt(dateParts[1], 10) - 1, dateParts[2], timeParts[0], timeParts[1]);
return date.getTime() / 1000;
}
return '';
}
/** base64_encode - Simple function to base64 encode a utf-8 string */
function base64_encode(string) {
return btoa(unescape(encodeURIComponent(string)));
}
/**
* getQueryString - this function will return the value
* of the get request variable defined as input.
* @param name - the variable name to return
*/
function getQueryString(name) {
var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}
/**
* delayKeyup - this function will delay the keyup to
* provide debouncing of input strokes on the keyboard
* this preventing your backend server from becoming overloaded
* usage: $("#yourid").delayKeyup(function(){ console.log('do something'); }, 500);
*
* @param name - the variable name to return
*/
$.fn.delayKeyup = function (callback, ms) {
var timer = 0;
if (ms === undefined || ms < 500) {
ms = keyup_delay;
}
$(this).on('keyup', function () {
clearTimeout(timer);
timer = setTimeout(callback, ms);
});
return $(this);
};
/**
* rebind - Shorthand for off(events).on(events, ...) to cleanly re-attach an
* event handler, optionally for a delegated selector.
*/
$.fn.rebind = function (events) {
// A string second argument is always a delegated selector (jQuery never
// treats a string as handler data), including the .on(map, selector) overload.
if (typeof arguments[1] === 'string') {
this.off(events, arguments[1]);
} else {
this.off(events);
}
return this.on.apply(this, arguments);
};
/**
* bindFirst - Function ensures that the event is found at the top
* of the event stack.
*/
$.fn.bindFirst = function (which, handler) {
var $el = $(this);
$el.off(which, handler);
$el.on(which, handler);
var events = $._data($el[0]).events;
var registered = events[which];
registered.unshift(registered.pop());
events[which] = registered;
};
/**
* replaceOptions - function replaces the options in a select dropdown
*/
$.fn.replaceOptions = function (options, selected) {
var self, $option;
this.empty();
self = this;
$.each(options, function (index, option) {
if (selected == option.value) {
$option = $('')
.attr('value', option.value)
.prop('selected', true)
.text(option.text);
} else {
$option = $('')
.attr('value', option.value)
.text(option.text);
}
self.append($option);
});
};
/**
* Enable or disable options that match the values passed
*
* @param {array} values The values to be updated
* @param {bool} optionEnabled Whether to enable or disable
* @param {function} valueCheckFunc The function be to called if anything changes
* @returns element
*/
$.fn.enableDisableOptions = function (values, optionEnabled, valueCheckFunc) {
var hasChanged = false;
if ($(this).length > 0) {
var self = $(this)[0];
$.each(self.options, function (index, option) {
var optionValue = $(this).val();
var optionDisabled = $(this).prop('disabled');
var optionIncluded = values.includes(optionValue);
/*
console.log('option ' + index + ' Name = ' + $(this).name);
console.log('option ' + index + ' Value = ' + optionValue);
console.log();
*/
var optionFinally = '---';
if (optionIncluded && optionDisabled == optionEnabled) {
hasChanged = true;
$(this).prop('disabled', !optionEnabled);
optionFinally = ($(this).prop('disabled') ? 'Yes':'No ');
}
/*
console.log('## option ' + index + ' (' + optionValue + ')' +
', Enabled = ' + (optionEnabled ? 'Yes' : 'No ') +
', Included = ' + (optionIncluded ? 'Yes' : 'No ') +
', Disabled = ' + (optionDisabled ? 'Yes' : 'No ') +
', Finally = ' + (optionFinally)
);
*/
});
}
/*
console.log('hasChanged = ' + hasChanged);
console.log();
*/
if (hasChanged) {
if (typeof valueCheckFunc == 'function') {
valueCheckFunc();
}
var selectMenu = $(this).selectmenu('instance');
if (selectMenu !== undefined) {
$(this).selectmenu('refresh');
}
if ($(this).hasClass("select2-hidden-accessible")) {
$(this).trigger('change.select2');
}
}
return $(this);
};
/**
* Enable options that match the values passed
*
* @param {array} values The values to be updated
* @param {function} valueCheckFunc The function be to called if anything changes
* @returns
*/
$.fn.disableOptions = function (values, valueCheckFunc) {
return $(this).enableDisableOptions(values, false, valueCheckFunc);
}
/**
* Disable options that match the values passed
*
* @param {array} values The values to be updated
* @param {function} valueCheckFunc The function be to called if anything changes
* @returns
*/
$.fn.enableOptions = function (values, valueCheckFunc) {
return $(this).enableDisableOptions(values, true, valueCheckFunc);
}
/**
* textWidth - This function will return the natural width of a string
* without any wrapping.
*/
$.fn.textWidth = function (text) {
var org = $(this);
var html = $('').text(text || org.text());
if (!text) {
html.css('font-family', org.css('font-family'));
html.css('font-weight', org.css('font-weight'));
html.css('font-size', org.css('font-size'));
html.css('padding', org.css('padding'));
html.css('margin', org.css('margin'));
}
$('body').append(html);
var width = html.width();
html.remove();
return width;
};
/**
* textBoxWidth - This function will return the natural width of a string
* without any wrapping.
*/
$.fn.textBoxWidth = function () {
var text;
var org = $(this);
if (org.val()) {
text = encodeURIComponent(org.val());
} else {
text = encodeURIComponent(org.text());
}
var html = $('' + text + '');
html.css('font-family', org.css('font-family'));
html.css('font-weight', org.css('font-weight'));
html.css('font-size', org.css('font-size'));
html.css('padding', org.css('padding'));
html.css('margin', org.css('margin'));
$('body').append(html);
var width = html.width();
html.remove();
return width;
};
/**
* classes - This function will return an array of all
* classes of an element
*/
$.fn.classes = function (callback) {
var classes = [];
$.each(this, function (i, v) {
var splitClassName = v.className.split(/\s+/);
for (var j in splitClassName) {
var className = splitClassName[j];
if (-1 === classes.indexOf(className)) {
classes.push(className);
}
}
});
if ('function' == typeof callback) {
for (var i in classes) {
callback(classes[i]);
}
}
return classes;
};
/**
* These three functions will set the cursor into
* a textbox or textarea and optionally select characters
*/
$.fn.setCursorPosition = function (position) {
if (this.length == 0) return this;
return this.setSelection(position, position);
};
$.fn.selectRange = function (start, end) {
if (!end) end = start;
return this.each(function () {
if (this.setSelectionRange) {
this.focus();
this.setSelectionRange(start, end);
} else if (this.createTextRange) {
var range = this.createTextRange();
range.collapse(true);
range.moveEnd('character', end);
range.moveStart('character', start);
range.select();
}
});
//return this;
};
$.fn.focusEnd = function () {
this.setCursorPosition($(this).val().length);
return this;
};
$.fn.getObjectDetails = function () {
var coords = {};
coords.width = $(this).outerWidth();
coords.height = $(this).outerHeight();
coords.left = 0;
coords.top = 0;
$(this).parentsUntil('body').each(function () {
coords.left += $(this).position().left;
coords.top += $(this).position().top;
});
return coords;
};
$.fn.serializeForm = function () {
var arrayData, objectData;
arrayData = this.serializeArray();
arrayData = arrayData.concat(
this.find('input[type=checkbox]:not(:checked):enabled[name]').map(function () {
return { "name": this.name, "value": $(this).is(':checked') ? 'on' : '' }
}).get());
objectData = {};
$.each(arrayData, function () {
var value;
if (this.value != null) {
value = this.value;
} else {
value = '';
}
if (objectData[this.name] != null) {
if (!objectData[this.name].push) {
objectData[this.name] = [objectData[this.name]];
}
objectData[this.name].push(value);
} else {
objectData[this.name] = value;
}
});
return objectData;
};
$.fn.serializeObject = function () {
var arrayData, objectData;
arrayData = this.serializeArray();
objectData = {};
$.each(arrayData, function () {
var value;
if (this.value != null) {
value = this.value;
} else {
value = '';
}
if (objectData[this.name] != null) {
if (!objectData[this.name].push) {
objectData[this.name] = [objectData[this.name]];
}
objectData[this.name].push(value);
} else {
objectData[this.name] = value;
}
});
return objectData;
};
// Borrowed from mustache.js
function escapeString(string) {
var entityMap = {
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'`': '`'
};
return String(string).replace(/[<>"'`]/g, function fromEntityMap(s) {
return entityMap[s];
});
}
// Plugin to apply numeric format for tablesorter
$.tablesorter.addParser({
id: 'numberFormat',
is: function (s) {
return /^[0-9]?[0-9,\.]*$/.test(s);
},
format: function (s) {
return $.tablesorter.formatFloat(s.replace(/,/g, ''));
},
type: 'numeric'
});
/** Mini jquery plugin to determine if an element has a scrollbar present */
(function ($) {
$.fn.hasScrollBar = function () {
return this.get(0).scrollHeight > this.outerHeight();
};
})(jQuery);
/** Mini jquery plugin to create a bind to show/hide events */
(function ($) {
$.each(['show', 'hide'], function (i, ev) {
var el = $.fn[ev];
$.fn[ev] = function () {
this.trigger(ev);
return el.apply(this, arguments);
};
});
})(jQuery);
// Helper function to get text dimensions
(function ($) {
$.textMetrics = function (el) {
var h = 0,
w = 0;
var div = document.createElement('div');
document.body.appendChild(div);
$(div).css({
position: 'absolute',
left: -1000,
top: -1000,
display: 'none'
});
$(div).html($(el).html());
var styles = ['font-size', 'font-style', 'font-weight', 'font-family', 'line-height', 'text-transform', 'letter-spacing'];
$(styles).each(function () {
var s = this.toString();
$(div).css(s, $(el).css(s));
});
var h = $(div).outerHeight();
var w = $(div).outerWidth();
$(div).remove();
var ret = {
height: h,
width: w
};
return ret;
}
})(jQuery);
// helper function which selects row range when shift key is pressed during click
function updateCheckboxes(checkboxes, clicked_element) {
var prev_checkbox = clicked_element.closest('table').find('[data-prev-check]:checkbox');
if (!prev_checkbox.length) {
return;
}
var check = prev_checkbox.attr('data-prev-check') == 'true';
var start = checkboxes.index(prev_checkbox);
var stop = checkboxes.index(clicked_element);
var i = Math.min(start, stop);
var j = Math.max(start, stop);
for (var k = i; k <= j; k++) {
var tr = $(checkboxes[k]).prop('checked', check).closest('tr');
if (check) {
tr.addClass('selected');
} else {
tr.removeClass('selected');
}
}
}
/** applySelectorVisibility - This function set's the initial visibility
* of graphs for creation. Is will scan the against preset variables
* taking action as required to enable or disable rows. */
function applySelectorVisibilityAndActions() {
// Change for accessibility
$('input[type="radio"]').rebind('click', function () {
if ($(this).is(':checked')) {
$(this).attr('aria-checked', 'true');
} else {
$(this).attr('aria-checked', 'false');
}
});
// Apply disabled/enabled status first for Graph Templates
$('tr[id^="gt_line"]').each(function (data) {
var id = $(this).attr('id');
var search = id.substr(7);
if ($.inArray(search, gt_created_graphs) >= 0) {
$(this).addClass('disabled_row');
$(this).find(':checkbox').prop('disabled', true);
}
});
// Create Actions for Rows
$('tr[id^="gt_line"].selectable:not(.disabled_row)').rebind('click', function (event) {
selectUpdateRow(event, $(this));
});
// Create Actions for Rows
$('tr[id^="line"].selectable').filter(':not(.disabled_row)').rebind('click', function (event) {
selectUpdateRow(event, $(this));
});
}
function disableSelection() {
$('tr.selectable').css('-webkit-user-select', 'none');
$('tr.selectable').css('-moz-user-select', 'none');
$('tr.selectable').css('-ms-user-select', 'none');
$('tr.selectable').css('-o-user-select', 'none');
$('tr.selectable').css('user-select', 'none');
}
function enableSelection() {
$('tr.selectable').css('-webkit-user-select', '');
$('tr.selectable').css('-moz-user-select', '');
$('tr.selectable').css('-ms-user-select', '');
$('tr.selectable').css('-o-user-select', '');
$('tr.selectable').css('user-select', '');
}
/**
* selectUpdateRow - Highlight a selectable row combined with checkbox
*
* @param event - The click event to support multiple selections
* @param element - The jQuery selected object
*/
function selectUpdateRow(event, element) {
var checkboxes = element.closest('table').find('input[type=checkbox]:not(:disabled)');
if (event.shiftKey) {
updateCheckboxes(checkboxes, element.find(':checkbox'));
} else {
element.toggleClass('selected');
if (element.hasClass('selected')) {
element.find(':checkbox').prop('checked', true).attr('aria-checked', 'true').attr('data-prev-check', 'true');
} else {
element.find(':checkbox').prop('checked', false).removeAttr('aria-checked').removeAttr('data-prev-check');
}
}
if (element.closest('table').find(':checked').length) {
disableSelection();
} else {
enableSelection();
}
}
/**
* dqUpdateDeps - When a user changes the Graph dropdown for a data query
* we have to check to see if those graphs are already created.
*
* @param snmp_query_id - The snmp query id the is current
*/
function dqUpdateDeps(snmp_query_id) {
$('tr[id^="dqline' + snmp_query_id + '_"]').addClass('selectable').removeClass('disabled_row').find(':checkbox').prop('disabled', false);
var snmp_query_graph_id = $('#sgg_' + snmp_query_id).val();
var removeSelectAll = false;
// Check if select all is clicked
var allChecked = $('#all_' + snmp_query_id).is(':checked');
// Next for Data Queries
$('tr[id^="dqline' + snmp_query_id + '_"]').each(function (data) {
var id = $(this).attr('id');
var pieces = id.split('_');
pieces[0].substr(6);
var hash = pieces[1];
if ($.inArray(hash, created_graphs[snmp_query_graph_id]) >= 0) {
if ($(this).hasClass('selected')) {
removeSelectAll = true;
}
$(this).addClass('disabled_row').removeClass('selected').removeClass('selectable');
$(this).find(':checkbox').prop('disabled', true).prop('checked', false);
} else {
removeSelectAll = true;
}
});
if (allChecked && removeSelectAll) {
$('#all_' + snmp_query_id).prop('checked', false);
}
$('tr[id^="dqline' + snmp_query_id + '_"]').not('.disabled_row').rebind('click', function (event) {
selectUpdateRow(event, $(this));
});
}
/**
* selectAll - This function will select all non-disabled rows
* @param attrib - The Graph Type either graph template, or data query
*/
function selectAll(attrib, checked) {
if (attrib == 'chk') {
if (checked == true) {
$('tr[id^="line"]:not(.disabled_row)').each(function (data) {
$(this).addClass('selected');
$(this).find(':checkbox').prop('checked', true).attr('aria-checked', 'true').attr('data-prev-check', 'true');
});
disableSelection();
} else {
$('tr[id^="line"]:not(.disabled_row)').each(function (data) {
$(this).removeClass('selected');
$(this).find(':checkbox').prop('checked', false).removeAttr('aria-checked').removeAttr('data-prev-check');
});
}
} else if (attrib == 'sg') {
if (checked == true) {
$('tr[id^="gt_line"]:not(.disabled_row)').each(function (data) {
$(this).addClass('selected');
$(this).find(':checkbox').prop('checked', true).attr('aria-checked', 'true').attr('data-prev-check', 'true');
});
disableSelection();
} else {
$('tr[id^="gt_line"]:not(.disabled_row)').each(function (data) {
$(this).removeClass('selected');
$(this).find(':checkbox').prop('checked', false).removeAttr('aria-checked').removeAttr('data-prev-check');
});
}
} else if (attrib.startsWith('sg_', 0)) {
var attribSplit = attrib.split('_');
var dq = attribSplit[1];
if (checked == true) {
$('tr[id^="dqline' + dq + '\_"]:not(.disabled_row)').each(function (data) {
$(this).addClass('selected');
$(this).find(':checkbox').prop('checked', true).attr('aria-checked', 'true').attr('data-prev-check', 'true');
});
disableSelection();
} else {
$('tr[id^="dqline' + dq + '\_"]:not(.disabled_row)').each(function (data) {
$(this).removeClass('selected');
$(this).find(':checkbox').prop('checked', false).removeAttr('aria-checked').removeAttr('data-prev-check');
});
}
} else {
if (checked == true) {
$('tr[id^="line_' + attrib + '"]:not(.disabled_row)').each(function (data) {
$(this).addClass('selected');
$(this).find(':checkbox').prop('checked', true).attr('aria-checked', 'true').attr('data-prev-check', 'true');
});
disableSelection();
} else {
$('tr[id^="line_' + attrib + '"]:not(.disabled_row)').each(function (data) {
$(this).removeClass('selected');
$(this).find(':checkbox').prop('checked', false).removeAttr('aria-checked').removeAttr('data-prev-check');
});
}
}
}
/* graph filtering */
function applyTimespanFilterChange() {
var href;
href = '?predefined_timespan=' + $('#predefined_timespan').val();
href += '&predefined_timeshift=' + $('#predefined_timeshift').val();
loadUrl({ url: href });
}
/**
* cactiReturnTo - This function simply returns to the previous page
* @param href - the previous page
*/
function cactiReturnTo(href) {
if (typeof href !== 'string' || href.trim() == '') {
href = document.referrer;
}
loadUrl({ url: href, force: true });
}
/**
* setupSelectmenuScrollClose - Close open select menus when their scroll
* container moves so the detached menu cannot remain over unrelated fields.
*/
function setupSelectmenuScrollClose() {
$('.cactiConsoleContentArea, .cactiGraphContentArea, .cactiGraphContentAreaPreview, .cactiTreeNavigationArea')
.add(window)
.rebind('scroll.cactiSelectmenu', function () {
if (!$('.ui-selectmenu-open').length) {
return;
}
$('select').each(function () {
if ($(this).selectmenu('instance') !== undefined) {
$(this).selectmenu('close');
}
});
});
}
/**
* applySkin - This function re-asserts all javascript behavior to a page
* that can't be set using a live attribute 'on()'
*/
function applySkin() {
pageName = basename($(location).attr('pathname'));
$('#messageContainer').remove();
/* Replace icons */
$('.fa-arrow-down').addClass('fa-chevron-down').removeClass('fa-arrow-down');
$('.fa-arrow-up').addClass('fa-chevron-up').removeClass('fa-arrow-up');
$('.fa-remove').addClass('fa-trash-o').removeClass('fa-remove');
if (!theme) {
theme = 'midwinter';
// debounce submits
$('form').on('submit', function () {
$('input[type="submit"], button[type="submit"]').not('.import, .export').prop('disabled', true);
});
} else {
$('input[type="submit"], input[type="button"], button').button();
// Handle re-index changes
$('fieldset.reindex_methods').controlgroup();
// debounce submits
$('form').on('submit', function () {
$('input[type="submit"], button[type="submit"]').not('.import, .export').button('disable');
});
}
// Setup blinking
clearInterval(applySkin.blinkTimer);
applySkin.blinkTimer = setInterval(blinkMe, 1200);
function blinkMe() {
$('.blink').each(function() {
var width = $(this).textBoxWidth();
$(this).css({ 'min-width': width });
$(this).fadeOut(900).fadeIn(900);
});
}
/* applySkin() runs more than once per page view (initial load, then again
* after every AJAX load/filter apply); tear down any select2 widget still
* attached before setSelectMenus()/the blocks below re-initialize, instead
* of relying only on the :not(.select2-hidden-accessible) guards to skip
* them, so a widget can never end up duplicated. Must run before
* setSelectMenus() so its catch-all init below still re-enhances plain
*