/* * FeisInfo: совместимость существующих страниц с Bootstrap 5 и DataTables 3. * Изменения сайта хранятся здесь, а не внутри файлов библиотек. */ (function (window, document, $) { 'use strict'; if (!$ || !window.bootstrap || !window.DataTable) { throw new Error('FeisInfo: Bootstrap, DataTables and jQuery must load first.'); } var DataTable = window.DataTable; var bootstrap = window.bootstrap; var originalDataTable = $.fn.dataTable; var originalDataTableApi = $.fn.DataTable; /* * Старые селекторы сохраняются рядом с современными классами. * Это позволяет оставить CSS сводки, табуляции и мобильных страниц. */ DataTable.ext.classes.container += ' dataTables_wrapper'; DataTable.ext.classes.empty.row += ' dataTables_empty'; DataTable.ext.classes.info.container += ' dataTables_info'; DataTable.ext.classes.length.container += ' dataTables_length'; DataTable.ext.classes.search.container += ' dataTables_filter'; DataTable.ext.classes.processing.container += ' dataTables_processing'; DataTable.ext.classes.paging.container += ' dataTables_paginate'; DataTable.ext.classes.paging.button += ' paginate_button'; /* * Старые страницы редактируют поля прямо в строках, включая строки * вне текущей страницы. Для них сохраняем создание всех DOM-строк. */ DataTable.defaults.deferRender = false; DataTable.defaults.column.orderSequence = ['asc', 'desc']; /* Не добавляем неожиданное выравнивание чисел поверх CSS сайта. */ DataTable.types().forEach(function (name) { DataTable.type(name, 'className', ''); }); /* * Сохраняем старые значения аргументов search(text): поиск без учёта * регистра, smart-поиск, без regex. В 3.1.3 undefined в позиционных * аргументах перезаписывает эти значения вместо сохранения defaults. */ function registerLegacySearch(name, originalSearch) { DataTable.Api.register(name, function (input, regex, smart, caseInsensitive) { if (input === undefined || (regex && typeof regex === 'object')) { return originalSearch.apply(this, arguments); } return originalSearch.call(this, input, { regex: regex === undefined || regex === null ? false : regex, smart: smart === undefined || smart === null ? true : smart, caseInsensitive: caseInsensitive === undefined || caseInsensitive === null ? true : caseInsensitive }); }); } var emptyApi = new DataTable.Api(); var columnSearch = Object.getPrototypeOf(emptyApi.columns()).search; registerLegacySearch('search()', DataTable.Api.prototype.search); registerLegacySearch('columns().search()', columnSearch); registerLegacySearch('column().search()', columnSearch); function markTableRows(tableNode) { var position = 0; var api = new DataTable.Api(tableNode); api.rows({ page: 'current' }).nodes().each(function (row) { $(row).removeClass('odd even').addClass(position % 2 ? 'even' : 'odd'); position++; }); } function restoreTableControlIds(api) { var tableId = api.table().node().id; var container = api.table().container(); [ ['.dt-paging', '_paginate'], ['.dt-search', '_filter'], ['.dt-length', '_length'], ['.dt-processing', '_processing'] ].forEach(function (item) { var control = container.querySelector(item[0]); if (control && !control.id) { control.id = tableId + item[1]; } }); } function prepareTableOptions(options) { if (options === undefined) { return options; } var prepared = $.extend({}, options); var drawCallback = prepared.drawCallback || prepared.fnDrawCallback; delete prepared.fnDrawCallback; /* В DataTables 3 this стал DataTable.Dom. Старые callbacks ждут jQuery. */ [ ['rowCallback', 'fnRowCallback'], ['headerCallback', 'fnHeaderCallback'], ['footerCallback', 'fnFooterCallback'], ['preDrawCallback', 'fnPreDrawCallback'], ['initComplete', 'fnInitComplete'], ['createdRow', 'fnCreatedRow'] ].forEach(function (names) { var callback = prepared[names[0]] || prepared[names[1]]; delete prepared[names[1]]; if (typeof callback !== 'function') { return; } prepared[names[0]] = function () { var api = this.api(); var $table = $(api.table().node()); $table.api = function () { return api; }; return callback.apply($table, arguments); }; }); prepared.drawCallback = function () { var api = this.api(); var tableNode = api.table().node(); restoreTableControlIds(api); markTableRows(tableNode); if (typeof drawCallback === 'function') { var $table = $(tableNode); $table.api = function () { return api; }; return drawCallback.apply($table, arguments); } }; return prepared; } function legacyDataTable(options) { return originalDataTable.call(this, prepareTableOptions(options)); } function legacyDataTableApi(options) { return originalDataTableApi.call(this, prepareTableOptions(options)); } /* Сохраняем Api, Responsive, ext, defaults и остальные публичные поля. */ $.extend(legacyDataTable, originalDataTable); $.extend(legacyDataTableApi, originalDataTableApi); $.fn.dataTable = legacyDataTable; $.fn.DataTable = legacyDataTableApi; /* Responsive 4 не всплывает до document: слушаем каждую таблицу. */ $(document).on('preInit.dt.feisFrameworks', function (event, settings) { var api = new DataTable.Api(settings); var $table = $(api.table().node()); $table.off('.feisFrameworks'); $table.on('draw.dt.feisFrameworks', function () { restoreTableControlIds(api); markTableRows(api.table().node()); }); $table.on('responsive-display.dt.feisFrameworks', function (event, api, row, shown) { $(row.node()).toggleClass('parent', !!shown); }); $table.on('destroy.dt.feisFrameworks', function () { $table.off('.feisFrameworks'); }); }); var legacyAttributes = [ 'toggle', 'dismiss', 'target', 'parent', 'backdrop', 'keyboard', 'focus', 'placement', 'html', 'container', 'trigger', 'animation', 'delay', 'title', 'content', 'ride', 'slide', 'slide-to', 'interval', 'wrap', 'touch', 'spy', 'offset', 'autohide', 'display', 'boundary' ]; function convertBootstrapAttributes(element) { if (!element || element.nodeType !== 1) { return; } legacyAttributes.forEach(function (name) { var oldName = 'data-' + name; var newName = 'data-bs-' + name; if (!element.hasAttribute(oldName) || element.hasAttribute(newName)) { return; } var value = element.getAttribute(oldName); /* Кнопки информации о сетах открывают окно через свой onclick. */ if (name === 'toggle' && value === 'modal') { var target = element.getAttribute('data-target') || element.getAttribute('href'); if (!target || target === '#' || target.charAt(0) !== '#') { return; } } if (name === 'placement' && value.indexOf('auto') === 0) { value = value.replace(/^auto\s*/, '') || 'top'; } element.setAttribute(newName, value); }); } function convertBootstrapTree(root) { convertBootstrapAttributes(root); if (root.querySelectorAll) { root.querySelectorAll('[data-toggle], [data-dismiss], .modal, [data-placement]').forEach( convertBootstrapAttributes ); } } function installBootstrapPlugin(name, Constructor) { var previous = $.fn[name]; var plugin = function (options) { var args = Array.prototype.slice.call(arguments, 1); return this.each(function () { /* Старое $(document).tooltip() не является Bootstrap-элементом. */ if (this.nodeType !== 1) { return; } convertBootstrapAttributes(this); var config = options && typeof options === 'object' ? $.extend({}, options) : {}; if (typeof config.placement === 'string' && config.placement.indexOf('auto') === 0) { config.placement = config.placement.replace(/^auto\s*/, '') || 'top'; } if (config.boundary === 'window') { config.boundary = 'clippingParents'; } if (name === 'collapse' && typeof options === 'string') { config.toggle = false; } var instance = Constructor.getOrCreateInstance(this, config); $(this).data('bs.' + name, instance); var method = options === 'destroy' ? 'dispose' : options; if (typeof method === 'string') { if (typeof instance[method] !== 'function') { throw new Error('FeisInfo: unknown Bootstrap method ' + name + '.' + method); } instance[method].apply(instance, args); } else if (name === 'modal' && config.show !== false) { instance.show(); } if (method === 'dispose') { $(this).removeData('bs.' + name); } }); }; plugin.Constructor = Constructor; plugin.noConflict = function () { $.fn[name] = previous; return plugin; }; $.fn[name] = plugin; } function installBootstrapPlugins() { [ ['modal', bootstrap.Modal], ['dropdown', bootstrap.Dropdown], ['collapse', bootstrap.Collapse], ['tab', bootstrap.Tab], ['tooltip', bootstrap.Tooltip], ['popover', bootstrap.Popover], ['alert', bootstrap.Alert], ['carousel', bootstrap.Carousel], ['toast', bootstrap.Toast] ].forEach(function (item) { installBootstrapPlugin(item[0], item[1]); }); /* * buttonset() вызывает button('widget') из jQuery UI. * Bootstrap регистрирует одноимённый плагин при DOMContentLoaded, * поэтому восстанавливаем UI-мост и сразу, и после его регистрации. * Bootstrap-кнопки доступны через bootstrapButton() и data-bs-toggle. */ installBootstrapPlugin('bootstrapButton', bootstrap.Button); if ($.widget && $.ui && $.ui.button) { $.widget.bridge('button', $.ui.button); } else { installBootstrapPlugin('button', bootstrap.Button); } } /* Доступны и до ready, и после собственной регистрации Bootstrap. */ installBootstrapPlugins(); document.addEventListener('DOMContentLoaded', installBootstrapPlugins); $(function () { convertBootstrapTree(document); var observer = new MutationObserver(function (changes) { changes.forEach(function (change) { change.addedNodes.forEach(convertBootstrapTree); }); }); observer.observe(document.body, { childList: true, subtree: true }); }); /* AJAX-вставка может быть нажата до выполнения MutationObserver. */ window.addEventListener('click', function (event) { var trigger = $(event.target).closest('[data-toggle], [data-dismiss]')[0]; if (trigger) { convertBootstrapTree(trigger); } }, true); })(window, document, window.jQuery);