UNPKG

datatables.net-columncontrol

Version:
1,488 lines (1,463 loc) 93 kB
/*! ColumnControl 1.0.5 * Copyright (c) SpryMedia Ltd - datatables.net/license * * SVG icons: ISC License * Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). * All other copyright (c) for Lucide are held by Lucide Contributors 2022. */ (function( factory ){ if ( typeof define === 'function' && define.amd ) { // AMD define( ['jquery', 'datatables.net'], function ( $ ) { return factory( $, window, document ); } ); } else if ( typeof exports === 'object' ) { // CommonJS var jq = require('jquery'); var cjsRequires = function (root, $) { if ( ! $.fn.dataTable ) { require('datatables.net')(root, $); } }; if (typeof window === 'undefined') { module.exports = function (root, $) { if ( ! root ) { // CommonJS environments without a window global must pass a // root. This will give an error otherwise root = window; } if ( ! $ ) { $ = jq( root ); } cjsRequires( root, $ ); return factory( $, root, root.document ); }; } else { cjsRequires( window, jq ); module.exports = factory( jq, window, window.document ); } } else { // Browser factory( jQuery, window, document ); } }(function( $, window, document ) { 'use strict'; var DataTable = $.fn.dataTable; function createElement(type, classes, text, children) { if (classes === void 0) { classes = []; } if (text === void 0) { text = null; } if (children === void 0) { children = []; } var el = document.createElement(type); addClass(el, classes); if (text) { el.innerHTML = text; } children.forEach(function (child) { el.appendChild(child); }); return el; } function addClass(el, classes) { if (!classes) { return; } if (!Array.isArray(classes)) { classes = [classes]; } classes.forEach(function (className) { if (el && className) { el.classList.add(className); } }); } // The SVG for many of these icons are from Lucide ( https://lucide.dev ), which are available // under the ISC License. There are a number of custom icons as well. These are optimised through // https://optimize.svgomg.net/ function wrap(paths) { return ('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + paths + '</svg>'); } var icons = { chevronRight: wrap('<path d="m9 18 6-6-6-6"/>'), // columns-3 columns: wrap('<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/>'), // Custom contains: wrap('<path d="M10 3h4v18h-4z"/><path d="M18 8h3v9h-3"/><path d="M6 17H3V8h3"/>'), empty: wrap('<circle cx="12" cy="12" r="10"/>'), ends: wrap('<path d="M21 3h-4v18h4z"/><path d="M13 8H3v9h10"/>'), // Customised equal: wrap('<line x1="5" x2="19" y1="9" y2="9"/><line x1="5" x2="19" y1="15" y2="15"/>'), greater: wrap('<path d="m9 18 6-6-6-6"/>'), // Custom greaterOrEqual: wrap('<path d="m9 16 6-6-6-6"/><path d="m9 21 6-6"/>'), less: wrap('<path d="m15 18-6-6 6-6"/>'), // Custom lessOrEqual: wrap('<path d="m15 16-6-6 6-6"/><path d="m15 21-6-6"/>'), menu: wrap('<line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/>'), // move-horizontal move: wrap('<line x1="12" x2="12" y1="3" y2="21"/><polyline points="8 8 4 12 8 16"/><polyline points="16 16 20 12 16 8"/>'), // arrow-left-from-line moveLeft: wrap('<path d="m9 6-6 6 6 6"/><path d="M3 12h14"/><path d="M21 19V5"/>'), // arrow-right-from-line moveRight: wrap('<path d="M3 5v14"/><path d="M21 12H7"/><path d="m15 18 6-6-6-6"/>'), // Custom notContains: wrap('<path d="M15 4 9 20"/><path d="M3 8h18v9H3z"/>'), notEmpty: wrap('<circle cx="12" cy="12" r="10"/><line x1="9" x2="15" y1="15" y2="9"/>'), notEqual: wrap('<path d="M5 9h14"/><path d="M5 15h14"/><path d="M15 5 9 19"/>'), // Custom orderAddAsc: wrap('<path d="M17 21v-8"/><path d="M3 4h6"/><path d="M3 8h9"/><path d="M3 12h10"/><path d="M13 17h8"/>'), // Custom orderAddDesc: wrap('<path d="M17 21v-8"/><path d="M3 4h12"/><path d="M3 8h9"/><path d="M3 12h6"/><path d="M13 17h8"/>'), orderAsc: wrap('<path d="m3 8 4-4 4 4"/><path d="M7 4v16"/><path d="M11 12h4"/><path d="M11 16h7"/><path d="M11 20h10"/>'), // Custom orderClear: wrap('<path d="m21 21-8-8"/><path d="M3 4h12"/><path d="M3 8h9"/><path d="M3 12h6"/><path d="m13 21 8-8"/>'), orderDesc: wrap('<path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="M11 4h10"/><path d="M11 8h7"/><path d="M11 12h4"/>'), // Custom orderRemove: wrap('<path d="M3 4h12"/><path d="M3 8h9"/><path d="M3 12h6"/><path d="M13 17h8"/>'), // Custom orderNone: wrap('<path d="m3 8 4-4 4 4"/><path d="m11 16-4 4-4-4"/><path d="M7 4v16"/><path d="M15 8h6"/><path d="M15 16h6"/><path d="M13 12h8"/>'), // search search: wrap('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>'), // search-x searchClear: wrap('<path d="m13.5 8.5-5 5"/><path d="m8.5 8.5 5 5"/><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>'), // Custom starts: wrap('<path d="M3 3h4v18H3z"/><path d="M11 8h10v9H11"/>'), // tick tick: wrap('<path d="M20 6 9 17l-5-5"/>'), // x x: wrap('<path d="M18 6 6 18"/><path d="m6 6 12 12"/>') }; /** * Close all or only other dropdowns * * @param e Event or null to close all others */ function close(e) { if (e === void 0) { e = null; } document.querySelectorAll('div.dtcc-dropdown').forEach(function (el) { if (e === null || !el.contains(e.target)) { el._close(); if (!e._closed) { e._closed = []; } e._closed.push(el); } }); } function getContainer(dt, btn) { return btn.closest('div.dtfh-floatingparent') || dt.table().container(); } /** * Position the dropdown relative to the button that activated it, with possible corrections * to make sure it is visible on the page. * * @param dropdown Dropdown element * @param dt Container DataTable * @param btn Button the dropdown emanates from */ function positionDropdown(dropdown, dt, btn) { var header = btn.closest('div.dt-column-header'); var container = getContainer(dt, btn); var headerStyle = getComputedStyle(header); var dropdownWidth = dropdown.offsetWidth; var position = relativePosition(container, btn); var left, top; top = position.top + btn.offsetHeight; if (headerStyle.flexDirection === 'row-reverse') { // Icon is on the left of the header - align the left hand sides left = position.left; } else { // Icon is on the right of the header - align the right hand sides left = position.left - dropdownWidth + btn.offsetWidth; } // Corrections - don't extend past the DataTable to the left and right var containerWidth = container.offsetWidth; if (left + dropdownWidth > containerWidth) { left -= left + dropdownWidth - containerWidth; } if (left < 0) { left = 0; } dropdown.style.top = top + 'px'; dropdown.style.left = left + 'px'; } /** * Display the dropdown in the document * * @param dropdown Dropdown element * @param dt Container DataTable * @param btn Button the dropdown emanates from * @returns Function to call when the dropdown should be removed from the document */ function attachDropdown(dropdown, dt, btn) { var dtContainer = getContainer(dt, btn.element()); dropdown._shown = true; dtContainer.append(dropdown); positionDropdown(dropdown, dt, btn.element()); // Note that this could be called when the dropdown has already been removed from the document // via another dropdown being shown. This will clean up the event on the next body click. var removeDropdown = function (e) { // Not in document, so just clean up the event handler if (!dropdown._shown) { document.body.removeEventListener('click', removeDropdown); return; } // If the click is inside the dropdown, ignore it - we don't want to immediately close if (e.target === dropdown || dropdown.contains(e.target)) { return; } dropdown._close(); document.body.removeEventListener('click', removeDropdown); }; document.body.addEventListener('click', removeDropdown); return removeDropdown; } /** * Get the position of an element, relative to a given parent. The origin MUST be under the * parent's tree. * * @param parent Parent element to get position relative to * @param origin Target element */ function relativePosition(parent, origin) { var top = 0; var left = 0; while (origin && origin !== parent && origin !== document.body) { top += origin.offsetTop; left += origin.offsetLeft; if (origin.scrollTop) { left -= origin.scrollTop; } if (origin.scrollLeft) { left -= origin.scrollLeft; } origin = origin.offsetParent; } return { top: top, left: left }; } /** * Function that will provide the keyboard navigation for the dropdown * * @param dropdown Dropdown element in question * @returns Function that can be bound to `keypress` */ function focusCapture(dropdown, host) { return function (e) { // Do nothing if not shown if (!dropdown._shown) { return; } // Focus trap for tab key var elements = Array.from(dropdown.querySelectorAll('a, button, input, select')); var active = document.activeElement; // An escape key should close the dropdown if (e.key === 'Escape') { dropdown._close(); host.focus(); // Restore focus to the host return; } else if (e.key !== 'Tab' || elements.length === 0) { // Anything other than tab we aren't interested in from here return; } if (!elements.includes(active)) { // If new focus is not inside the popover we want to drag it back in elements[0].focus(); e.preventDefault(); } else if (e.shiftKey) { // Reverse tabbing order when shift key is pressed if (active === elements[0]) { elements[elements.length - 1].focus(); e.preventDefault(); } } else { if (active === elements[elements.length - 1]) { elements[0].focus(); e.preventDefault(); } } }; } var dropdownContent = { classes: { container: 'dtcc-dropdown', liner: 'dtcc-dropdown-liner' }, defaults: { className: 'dropdown', content: [], icon: 'menu', text: 'More...' }, init: function (config) { var dt = this.dt(); var dropdown = createElement('div', dropdownContent.classes.container, '', [ createElement('div', dropdownContent.classes.liner) ]); dropdown._shown = false; dropdown._close = function () { dropdown.remove(); dropdown._shown = false; }; dropdown.setAttribute('role', 'dialog'); dropdown.setAttribute('aria-label', dt.i18n('columnControl.dropdown', config.text)); // When FixedHeader is used, the transition between states messes up positioning, so if // shown we just reattach the dropdown. dt.on('fixedheader-mode', function () { if (dropdown._shown) { attachDropdown(dropdown, dt, config._parents ? config._parents[0] : btn); } }); // A liner element allows more styling options, so the contents go inside this var liner = dropdown.childNodes[0]; var btn = new Button(dt) .text(dt.i18n('columnControl.dropdown', config.text)) .icon(config.icon) .className(config.className) .dropdownDisplay(liner) .handler(function (e) { // Do nothing if our dropdown was just closed as part of the event (i.e. allow // the button to toggle it closed) if (e._closed && e._closed.includes(dropdown)) { return; } attachDropdown(dropdown, dt, config._parents ? config._parents[0] : btn); // When activated using a key - auto focus on the first item in the popover var focusable = dropdown.querySelector('input, a, button'); console.log(e.type, e); if (focusable && e.type === 'keypress') { focusable.focus(); } }); btn.element().setAttribute('aria-haspopup', 'dialog'); // Add the content for the dropdown for (var i = 0; i < config.content.length; i++) { var content = this.resolve(config.content[i]); // For nested items we need to keep a reference to the top level so the sub-levels // can communicate back - e.g. active or positioned relative to that top level. if (!content.config._parents) { content.config._parents = []; } content.config._parents.push(btn); var el = content.plugin.init.call(this, content.config); liner.appendChild(el); } // For nested dropdowns, add an extra icon element to show that it will dropdown further if (config._parents && config._parents.length) { btn.extra('chevronRight'); } // Reposition if needed dt.on('columns-reordered', function () { positionDropdown(dropdown, dt, btn.element()); }); // Focus capture events var capture = focusCapture(dropdown, btn.element()); document.body.addEventListener('keydown', capture); dt.on('destroy', function () { document.body.removeEventListener('keydown', capture); }); return btn.element(); } }; var _namespace = 0; var Button = /** @class */ (function () { /** * Create a new button for use in ColumnControl contents. Buttons created by this class can be * used at the top level in the header or in a dropdown. */ function Button(dt) { this._s = { active: false, activeList: [], buttonClick: null, dt: null, enabled: true, label: '', namespace: '', value: null }; this._s.dt = dt; this._dom = { button: createElement('button', Button.classes.container), dropdownDisplay: null, extra: createElement('span', 'dtcc-button-extra'), icon: createElement('span', 'dtcc-button-icon'), state: createElement('span', 'dtcc-button-state'), text: createElement('span', 'dtcc-button-text') }; this._dom.button.append(this._dom.icon); this._dom.button.append(this._dom.text); this._dom.button.append(this._dom.state); this._dom.button.append(this._dom.extra); // Default state is enabled this.enable(true); } Button.prototype.active = function (active) { if (active === undefined) { return this._s.active; } this._s.active = active; this._checkActive(); return this; }; /** * A button can be marked as active by any of its sub-buttons (i.e. if it is a dropdown) * and each one needs to be able to enable this button without effecting the active state * trigged by any other sub-buttons. This method provides a way to do that. * * @param unique Unique id for the activate state * @param active If it is active * @returns Button instance */ Button.prototype.activeList = function (unique, active) { this._s.activeList[unique] = active; this._checkActive(); return this; }; /** * Scan over the dropdown element looking for any visible content. If there isn't any then * we hide this button. * * @returns Button instance */ Button.prototype.checkDisplay = function () { var visible = 0; var children = this._dom.dropdownDisplay.childNodes; for (var i = 0; i < children.length; i++) { // No need to getComputedStyle since if a button is hidden, it was done with JS writing // to style.display, so we can check against that. if (children[i].style.display !== 'none') { visible++; } } if (visible === 0) { this._dom.button.style.display = 'none'; } return this; }; /** * Set the class name for the button * * @param className Class name * @returns Button instance */ Button.prototype.className = function (className) { this._dom.button.classList.add('dtcc-button_' + className); return this; }; /** * Destroy the button, cleaning up event listeners */ Button.prototype.destroy = function () { if (this._s.buttonClick) { this._dom.button.removeEventListener('click', this._s.buttonClick); this._dom.button.removeEventListener('keypress', this._s.buttonClick); } if (this._s.namespace) { this._s.dt.off('destroy.' + this._s.namespace); } }; /** * Relevant for drop downs only. When a button in a dropdown is hidden, we might want to * hide the host button as well (if it has nothing else to show). For that we need to know * what the dropdown element is. * * @param el Element that can be used for telling us about drop down elements. * @returns Button instance */ Button.prototype.dropdownDisplay = function (el) { this._dom.dropdownDisplay = el; return this; }; /** * Get the DOM Button element to attach into the document * * @returns The Button element */ Button.prototype.element = function () { return this._dom.button; }; /** * Set if the button should be enabled or not. * * @param enable Toggle the enable state * @returns Button instance */ Button.prototype.enable = function (enable) { this._dom.button.classList.toggle('dtcc-button_disabled', !enable); this._s.enabled = enable; return this; }; /** * Set the extra information icon * * @param icon Icon name * @returns Button instance */ Button.prototype.extra = function (icon) { this._dom.extra.innerHTML = icon ? icons[icon] : ''; return this; }; /** * Set the event handler for when the button is activated * * @param fn Event handler * @returns Button instance */ Button.prototype.handler = function (fn) { var _this = this; var buttonClick = function (e) { // Close any dropdowns which are already open close(e); // Stop bubbling to the DataTables default header, which might still be enabled e.stopPropagation(); e.preventDefault(); if (_this._s.enabled) { fn(e); } }; this._s.buttonClick = buttonClick; this._s.namespace = 'dtcc-' + _namespace++; this._dom.button.addEventListener('click', buttonClick); this._dom.button.addEventListener('keypress', buttonClick); // Use a unique namespace to be able to easily remove per button this._s.dt.on('destroy.' + this._s.namespace, function () { _this.destroy(); }); return this; }; /** * Set the icon to display in the button * * @param icon Icon name * @returns Button instance */ Button.prototype.icon = function (icon) { this._dom.icon.innerHTML = icon ? icons[icon] : ''; return this; }; Button.prototype.text = function (text) { if (text === undefined) { return this._s.label; } this._dom.text.innerHTML = text; this._s.label = text; // for fast retrieval this._dom.button.setAttribute('aria-label', text); return this; }; Button.prototype.value = function (val) { if (val === undefined) { return this._s.value; } this._s.value = val; return this; }; /** * Check if anything is making this button active * * @returns Self for chaining */ Button.prototype._checkActive = function () { if (this._s.active === true || Object.values(this._s.activeList).includes(true)) { this._dom.state.innerHTML = icons.tick; this._dom.button.classList.add('dtcc-button_active'); } else { this._dom.state.innerHTML = ''; this._dom.button.classList.remove('dtcc-button_active'); } return this; }; Button.classes = { container: 'dtcc-button' }; return Button; }()); var CheckList = /** @class */ (function () { /** * Container for a list of buttons */ function CheckList(dt, opts) { var _this = this; this._s = { buttons: [], dt: null, handler: function () { }, search: '' }; this._s.dt = dt; this._dom = { buttons: createElement('div', 'dtcc-list-buttons'), container: createElement('div', CheckList.classes.container), controls: createElement('div', 'dtcc-list-controls'), title: createElement('div', 'dtcc-list-title'), selectAll: createElement('button', 'dtcc-list-selectAll', dt.i18n('columnControl.list.all', 'Select all')), selectAllCount: createElement('span'), selectNone: createElement('button', 'dtcc-list-selectNone', dt.i18n('columnControl.list.none', 'Deselect')), selectNoneCount: createElement('span'), search: createElement('input', CheckList.classes.input) }; var dom = this._dom; dom.search.setAttribute('type', 'text'); dom.container.append(dom.title); dom.container.append(dom.controls); dom.container.append(dom.buttons); if (opts.select) { dom.controls.append(dom.selectAll); dom.controls.append(dom.selectNone); dom.selectAll.append(dom.selectAllCount); dom.selectNone.append(dom.selectNoneCount); } // Events var searchInput = function () { _this._s.search = dom.search.value; _this._redraw(); }; var selectAllClick = function (e) { _this.selectAll(); _this._s.handler(e, null, _this._s.buttons, true); _this._updateCount(); }; var selectNoneClick = function (e) { _this.selectNone(); _this._s.handler(e, null, _this._s.buttons, true); _this._updateCount(); }; if (opts.search) { dom.controls.append(dom.search); dom.search.setAttribute('placeholder', dt.i18n('columnControl.list.search', 'Search...')); dom.search.addEventListener('input', searchInput); } dom.selectAll.addEventListener('click', selectAllClick); dom.selectNone.addEventListener('click', selectNoneClick); dt.on('destroy', function () { dom.selectAll.removeEventListener('click', selectAllClick); dom.selectNone.removeEventListener('click', selectNoneClick); dom.search.removeEventListener('input', searchInput); }); } /** * Add one or more buttons to the list * * @param options Configuration for the button(s) to add * @returns Self for chaining */ CheckList.prototype.add = function (options, update) { var _this = this; if (!Array.isArray(options)) { options = [options]; } var _loop_1 = function (i) { var option = options[i]; var btn = new Button(this_1._s.dt) .active(option.active || false) .handler(function (e) { _this._s.handler(e, btn, _this._s.buttons, true); _this._updateCount(); }) .icon(option.icon || '') .text(option.label !== '' ? option.label : this_1._s.dt.i18n('columnControl.list.emptyOption', 'Empty')) .value(option.value); if (option.label === '') { btn.className('empty'); } this_1._s.buttons.push(btn); }; var this_1 = this; for (var i = 0; i < options.length; i++) { _loop_1(i); } var count = this._s.buttons.length; if (update === true || update === undefined) { this._dom.selectAllCount.innerHTML = count ? '(' + count + ')' : ''; this._redraw(); } return this; }; /** * Find a button with a given value * * @param val Value to search for * @returns Found button */ CheckList.prototype.button = function (val) { var buttons = this._s.buttons; for (var i = 0; i < buttons.length; i++) { if (buttons[i].value() === val) { return buttons[i]; } } return null; }; /** * Remove all buttons from the list * * @returns Self for chaining */ CheckList.prototype.clear = function () { // Clean up the buttons for (var i = 0; i < this._s.buttons.length; i++) { this._s.buttons[i].destroy(); } // Then empty them out this._dom.buttons.replaceChildren(); this._s.buttons.length = 0; return this; }; /** * Get the DOM container element to attach into the document * * @returns Container */ CheckList.prototype.element = function () { return this._dom.container; }; /** * Set the event handler for what happens when a button is clicked * * @param fn Event handler */ CheckList.prototype.handler = function (fn) { this._s.handler = fn; return this; }; /** * Indicate that this is a search control and should listen for corresponding events * * @param dt DataTable instance * @param idx Column index */ CheckList.prototype.searchListener = function (dt, parent) { var _this = this; // Column control search clearing (column().ccSearchClear() method) dt.on('cc-search-clear', function (e, colIdx) { if (colIdx === parent.idx()) { _this.selectNone(); _this._s.handler(e, null, _this._s.buttons, false); _this._s.search = ''; _this._dom.search.value = ''; _this._redraw(); _this._updateCount(); } }); return this; }; /** * Select all buttons * * @returns Self for chaining */ CheckList.prototype.selectAll = function () { for (var i = 0; i < this._s.buttons.length; i++) { this._s.buttons[i].active(true); } return this; }; /** * Deselect all buttons * * @returns Self for chaining */ CheckList.prototype.selectNone = function () { for (var i = 0; i < this._s.buttons.length; i++) { this._s.buttons[i].active(false); } return this; }; /** * Set the list's title * * @param title Display title * @returns Button instance */ CheckList.prototype.title = function (title) { this._dom.title.innerHTML = title; return this; }; CheckList.prototype.values = function (values) { var i; var result = []; var buttons = this._s.buttons; if (values !== undefined) { for (i = 0; i < buttons.length; i++) { if (values.includes(buttons[i].value())) { buttons[i].active(true); } } this._updateCount(); return this; } for (i = 0; i < buttons.length; i++) { if (buttons[i].active()) { result.push(buttons[i].value()); } } return result; }; /** * Update the deselect counter */ CheckList.prototype._updateCount = function () { var count = this.values().length; this._dom.selectNoneCount.innerHTML = count ? '(' + count + ')' : ''; }; /** * Add the buttons to the page - taking into account filtering */ CheckList.prototype._redraw = function () { var buttons = this._s.buttons; var el = this._dom.buttons; var searchTerm = this._s.search.toLowerCase(); el.replaceChildren(); for (var i = 0; i < buttons.length; i++) { var btn = buttons[i]; if (!searchTerm || btn .text() .toLowerCase() .includes(searchTerm)) { el.appendChild(btn.element()); } } }; CheckList.classes = { container: 'dtcc-list', input: 'dtcc-list-search' }; return CheckList; }()); var colVis = { defaults: { className: 'colVis', columns: '', search: false, select: false, title: 'Column visibility' }, init: function (config) { var dt = this.dt(); var checkList = new CheckList(dt, { search: config.search, select: config.select }) .title(dt.i18n('columnControl.colVis', config.title)) .handler(function (e, btn, buttons) { if (btn) { btn.active(!btn.active()); } apply(buttons); }); // Need to apply in a loop to allow for select all / select none var apply = function (buttons) { for (var i = 0; i < buttons.length; i++) { var btn = buttons[i]; var idx = btn.value(); var col = dt.column(idx); if (btn.active() && !col.visible()) { col.visible(true); } else if (!btn.active() && col.visible()) { col.visible(false); } } }; var rebuild = function () { var columns = dt.columns(config.columns); columns.every(function () { checkList.add({ active: this.visible(), label: this.title(), value: this.index() }); }); }; rebuild(); dt.on('column-visibility', function (e, s, colIdx, state) { var btn = checkList.button(colIdx); if (btn) { btn.active(state); } }); dt.on('columns-reordered', function (e, details) { checkList.clear(); rebuild(); }); return checkList.element(); } }; var colVisDropdown = { defaults: { className: 'colVis', columns: '', search: false, select: false, text: 'Column visibility', title: 'Column visibility' }, extend: function (config) { var dt = this.dt(); return { extend: 'dropdown', icon: 'columns', text: dt.i18n('columnControl.colVisDropdown', config.text), content: [ Object.assign(config, { extend: 'colVis' }) ] }; } }; var reorder = { defaults: { className: 'reorder', icon: 'move', text: 'Reorder columns' }, init: function (config) { var _this = this; var dt = this.dt(); var btn = new Button(dt) .text(dt.i18n('columnControl.reorder', config.text)) .icon(config.icon) .className(config.className) .handler(function () { var idx = _this.idx(); if (idx > 0) { dt.colReorder.move(idx, idx - 1); } }); if (this.idx() === 0) { btn.enable(false); } dt.on('columns-reordered', function (e, details) { btn.enable(_this.idx() > 0); }); // If ColReorder wasn't initialised on this DataTable, then we need to add it if (!dt.init().colReorder) { new DataTable.ColReorder(dt, {}); } return btn.element(); } }; var reorderLeft = { defaults: { className: 'reorderLeft', icon: 'moveLeft', text: 'Move column left' }, init: function (config) { var _this = this; var dt = this.dt(); var btn = new Button(dt) .text(dt.i18n('columnControl.reorderLeft', config.text)) .icon(config.icon) .className(config.className) .handler(function () { var idx = _this.idx(); // TODO account for visibility if (idx > 0) { dt.colReorder.move(idx, idx - 1); } }); if (this.idx() === 0) { btn.enable(false); } dt.on('columns-reordered', function (e, details) { btn.enable(_this.idx() > 0); }); return btn.element(); } }; var reorderRight = { defaults: { className: 'reorderRight', icon: 'moveRight', text: 'Move column right' }, init: function (config) { var _this = this; var dt = this.dt(); var btn = new Button(dt) .text(dt.i18n('columnControl.reorderRight', config.text)) .icon(config.icon) .className(config.className) .handler(function () { var idx = _this.idx(); if (idx < dt.columns().count() - 1) { dt.colReorder.move(idx, idx + 1); } }); if (this.idx() === dt.columns().count() - 1) { btn.enable(false); } dt.on('columns-reordered', function (e, details) { btn.enable(_this.idx() < dt.columns().count() - 1); }); return btn.element(); } }; var order = { defaults: { className: 'order', iconAsc: 'orderAsc', iconDesc: 'orderDesc', iconNone: 'orderNone', statusOnly: false, text: 'Toggle ordering' }, init: function (config) { var _this = this; var dt = this.dt(); var btn = new Button(dt) .text(dt.i18n('columnControl.order', config.text)) .icon('orderAsc') .className(config.className); if (!config.statusOnly) { dt.order.listener(btn.element(), DataTable.versionCheck('2.3.2') ? function () { return [_this.idx()]; } : this.idx(), function () { }); } dt.on('order', function (e, s, order) { var found = order.find(function (o) { return o.col === _this.idx(); }); if (!found) { btn.active(false).icon(config.iconNone); } else if (found.dir === 'asc') { btn.active(true).icon(config.iconAsc); } else if (found.dir === 'desc') { btn.active(true).icon(config.iconDesc); } }); return btn.element(); } }; var orderAddAsc = { defaults: { className: 'orderAddAsc', icon: 'orderAddAsc', text: 'Add Sort Ascending' }, init: function (config) { var _this = this; var dt = this.dt(); var btn = new Button(dt) .text(dt.i18n('columnControl.orderAddAsc', config.text)) .icon(config.icon) .className(config.className) .handler(function () { var order = dt.order(); order.push([_this.idx(), 'asc']); dt.draw(); }); dt.on('order', function (e, s, order) { var found = order.some(function (o) { return o.col === _this.idx(); }); btn.enable(!found); }); return btn.element(); } }; var orderAddDesc = { defaults: { className: 'orderAddDesc', icon: 'orderAddDesc', text: 'Add Sort Descending' }, init: function (config) { var _this = this; var dt = this.dt(); var btn = new Button(dt) .text(dt.i18n('columnControl.orderAddDesc', config.text)) .icon(config.icon) .className(config.className) .handler(function () { var order = dt.order(); order.push([_this.idx(), 'desc']); dt.draw(); }); dt.on('order', function (e, s, order) { var found = order.some(function (o) { return o.col === _this.idx(); }); btn.enable(!found); }); return btn.element(); } }; var orderAsc = { defaults: { className: 'orderAsc', icon: 'orderAsc', text: 'Sort Ascending' }, init: function (config) { var _this = this; var dt = this.dt(); var btn = new Button(dt) .text(dt.i18n('columnControl.orderAsc', config.text)) .icon(config.icon) .className(config.className) .handler(function () { _this.dt() .order([ { idx: _this.idx(), dir: 'asc' } ]) .draw(); }); dt.on('order', function (e, s, order) { var found = order.some(function (o) { return o.col === _this.idx() && o.dir === 'asc'; }); btn.active(found); }); return btn.element(); } }; var orderClear = { defaults: { className: 'orderClear', icon: 'orderClear', text: 'Clear sort' }, init: function (config) { var dt = this.dt(); var btn = new Button(dt) .text(dt.i18n('columnControl.orderClear', config.text)) .icon(config.icon) .className(config.className) .handler(function () { dt.order([]).draw(); }); dt.on('order', function (e, s, order) { btn.enable(order.length > 0); }); if (dt.order().length === 0) { btn.enable(false); } return btn.element(); } }; var orderDesc = { defaults: { className: 'orderDesc', icon: 'orderDesc', text: 'Sort Descending' }, init: function (config) { var _this = this; var dt = this.dt(); var btn = new Button(dt) .text(dt.i18n('columnControl.orderDesc', config.text)) .icon(config.icon) .className(config.className) .handler(function () { _this.dt() .order([ { idx: _this.idx(), dir: 'desc' } ]) .draw(); }); dt.on('order', function (e, s, order) { var found = order.some(function (o) { return o.col === _this.idx() && o.dir === 'desc'; }); btn.active(found); }); return btn.element(); } }; var orderRemove = { defaults: { className: 'orderRemove', icon: 'orderRemove', text: 'Remove from sort' }, init: function (config) { var _this = this; var dt = this.dt(); var btn = new Button(dt) .text(dt.i18n('columnControl.orderRemove', config.text)) .icon(config.icon) .className(config.className) .handler(function () { // Remove the current column from the ordering array, then reorder the table var order = dt.order(); var idx = order.findIndex(function (o) { return o[0] === _this.idx(); }); order.splice(idx, 1); dt.order(order).draw(); }); dt.on('order', function (e, s, order) { var found = order.some(function (o) { return o.col === _this.idx(); }); btn.enable(found); }); btn.enable(false); return btn.element(); } }; var orderStatus = { defaults: { className: 'order', iconAsc: 'orderAsc', iconDesc: 'orderDesc', iconNone: 'orderNone', statusOnly: true, text: 'Sort status' }, extend: function (config) { return Object.assign(config, { extend: 'order' }); } }; var SearchInput = /** @class */ (function () { /** * Create a container element, for consistent DOM structure and styling */ function SearchInput(dt, idx) { var _this = this; this._type = 'text'; this._dt = dt; this._idx = idx; this._dom = { clear: createElement('span', 'dtcc-search-clear', icons['x']), container: createElement('div', SearchInput.classes.container), typeIcon: createElement('div', 'dtcc-search-type-icon'), searchIcon: createElement('div', 'dtcc-search-icon', icons['search']), input: createElement('input', SearchInput.classes.input), inputs: createElement('div'), select: createElement('select', SearchInput.classes.select), title: createElement('div', 'dtcc-search-title') }; var dom = this._dom; var originalIdx = idx; dom.input.setAttribute('type', 'text'); dom.container.append(dom.title, dom.inputs); dom.inputs.append(dom.typeIcon, dom.select, dom.searchIcon, dom.clear, dom.input); // Listeners var inputInput = function () { _this.runSearch(); }; var selectInput = function () { dom.typeIcon.innerHTML = icons[dom.select.value]; _this.runSearch(); }; var clearClick = function () { _this.clear(); }; dom.input.addEventListener('input', inputInput); dom.select.addEventListener('input', selectInput); dom.clear.addEventListener('click', clearClick); dt.on('destroy', function () { dom.input.removeEventListener('input', inputInput); dom.select.removeEventListener('input', selectInput); dom.clear.removeEventListener('click', clearClick); }); // State handling - all components that use this class have the same state saving structure // so shared handling can be performed here. dt.on('stateSaveParams', function (e, s, data) { if (!data.columnControl) { data.columnControl = {}; } if (!data.columnControl[idx]) { data.columnControl[idx] = {}; } data.columnControl[idx].searchInput = { logic: dom.select.value, type: _this._type, value: dom.input.value }; }); dt.on('stateLoaded', function (e, s, state) { _this._stateLoad(state); }); // Same as for ColumnControl - reassign a column index if needed. dt.on('columns-reordered', function (e, details) { _this._idx = dt.colReorder.transpose(originalIdx, 'fromOriginal'); }); // Column control search clearing (column().ccSearchClear() method) dt.on('cc-search-clear', function (e, colIdx) { if (colIdx === _this._idx) { // Don't want an automatic redraw on this event _this._loadingState = true; _this.clear(); _this._loadingState = false; } }); } /** * Add a class to the container * * @param name Class name to add * @returns Self for chaining */ SearchInput.prototype.addClass = function (name) { this._dom.container.classList.add(name); return this; }; /** * Clear any applied search * * @returns Self for chaining */ SearchInput.prototype.clear = function () { this.set(this._dom.select.children[0].getAttribute('value'), ''); return this; }; /** * Set the clear icon feature can be used or not * * @param set Flag * @returns Self for chaining */ SearchInput.prototype.clearable = function (set) { // Note there is no add here as it is added by default and never used after setup, so // no need. if (!set) { this._dom.clear.remove(); } return this; }; /** * Get the container element * * @returns The container element */ SearchInput.prototype.element = function () { return this._dom.container; }; /** * Get the HTML input element for this control * * @returns HTML Input element */ SearchInput.prototype.input = function () { return this._dom.input; }; /** * Set the list of options for the dropdown * * @param opts List of options * @returns Self for chaining */ SearchInput.prototype.options = function (opts) { var select = this._dom.select; for (var i = 0; i < opts.length; i++) { select.add(new Option(opts[i].label, opts[i].value)); } // Initial icon this._dom.typeIcon.innerHTML = icons[opts[0].value]; return this; }; /** * Set the placeholder attribute for the input element * * @param placeholder Placeholder string * @returns Self for chaining */ SearchInput.prototype.placeholder = function (placeholder) { if (placeholder) { var columnTitle = this._dt.column(this._idx).title(); this._dom.input.placeholder = placeholder.replace('[title]', columnTitle); } return this; }; /** * Run the search method */ SearchInput.prototype.runSearch = function () { var dom = this._dom; var isActive = dom.select.value === 'empty' || dom.select.value === 'notEmpty' || dom.input.value !== ''; dom.container.classList.toggle('dtcc-search_active', isActive); if (this._search && (this._lastValue !== dom.input.value || this._lastType !== dom.select.value)) { this._search(dom.select.value, dom.input.value, this._loadingState); this._lastValue = dom.input.value; this._lastType = dom.select.value; } }; /** * Set the function that will be run when a search operation is required. Note that this can * trigger the function to run if there is a saved state. * * @param fn Search callback * @returns Self for chaining */ SearchInput.prototype.search = function (fn) { this._search = fn; // If there is a saved state, load it now that set up is done. this._stateLoad(this._dt.state.loaded()); return this; }; /** * Set a value for the search input * * @param logic Logic type * @param val Value * @returns Self for chaining */ SearchInput.prototype.set = function (logic, val) { var dom = this._dom; dom.input.value = val; dom.select.value = logic; dom.typeIcon.innerHTML = icons[dom.select.value]; this.runSearch(); return this; }; /** * Set the text that will be shown as the title for the control * * @param text Set the title text * @returns Self for chaining */ SearchInput.prototype.title = function (text) { if (text) { var columnTitle = this._dt.column(this._idx).title(); this._dom.title.innerHTML = text.replace('[title]', columnTitle); } return this; }; /** * Set the title attribute for the input element * * @param title Title attribute string * @returns Self for chaining */ SearchInput.prototype.titleAttr = function (title) { if (title) { var columnTitle = this._dt.column(this._idx).title(); this._dom.input.title = title.replace('[title]', columnTitle); } return this; }; SearchInput.prototype.type = function (t) { this._type = t; return this; }; /** * Load a DataTables state * * @param state State object being loaded */ SearchInput.prototype._stateLoad = function (state) { var _a, _b; var dom = this._dom; var idx = this._idx; var loadedState = (_b = (_a = state === null || state === void 0 ? void 0 : state.columnControl) === null || _a === void 0 ? void 0 : _a[idx]) === null || _b === void 0 ? void 0 : _b.searchInput; if (loadedState) { // The search callback needs to know if we are loading an existing state or not // so it can determine if it needs to draw the table. If it was a user input, then // it redraws, if it was a state load, then there should be no redraw. this._loadingState = true; dom.select.value = loadedState.logic; dom.input.value = l