datatables.net-columncontrol
Version:
ColumnControl for DataTables
1,543 lines (1,518 loc) • 112 kB
JavaScript
/*! ColumnControl 2.0.0 for DataTables
* 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.
*/
import DataTable, { Dom, Api } from 'datatables.net';
function createElement(type, classes = [], text = null, children = []) {
let el = document.createElement(type);
addClass(el, classes);
if (text) {
el.innerHTML = text;
}
children.forEach((child) => {
el.appendChild(child);
});
return el;
}
function addClass(el, classes) {
if (!classes) {
return;
}
if (!Array.isArray(classes)) {
classes = [classes];
}
classes.forEach((className) => {
if (el && className) {
el.classList.add(className);
}
});
}
/**
* Close all or only other dropdowns
*
* @param e Event or null to close all others
*/
function close(e = null) {
document.querySelectorAll('div.dtcc-dropdown').forEach((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) {
let header = btn.closest('div.dt-column-header');
let container = getContainer(dt, btn);
let headerStyle = getComputedStyle(header);
let dropdownWidth = dropdown.offsetWidth;
let position = relativePosition(container, btn);
let 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
let 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
* @param autoFocus Selector to run to find if we can focus on an item
* automatically
* @returns Function to call when the dropdown should be removed from the
* document
*/
function attachDropdown(dropdown, dt, btn, autoFocus) {
let dtContainer = getContainer(dt, btn.element());
dropdown._shown = true;
dtContainer.append(dropdown);
positionDropdown(dropdown, dt, btn.element());
btn.element().setAttribute('aria-expanded', 'true');
// 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.
let removeDropdown = (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;
}
// If there is currently a datetime picker visible on the page, assume that it belongs to
// this dropdown. Don't want to close while operating on the picker.
let datetime = document.querySelector('div.dt-datetime');
if (datetime && (e.target === datetime || datetime.contains(e.target))) {
return;
}
dropdown._close();
document.body.removeEventListener('click', removeDropdown);
};
document.body.addEventListener('click', removeDropdown);
// Focus on an input if we can
if (autoFocus) {
let el = dropdown.querySelector(autoFocus);
if (el) {
el.focus();
}
}
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) {
let top = 0;
let 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,
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();
}
}
};
}
const dropdownContent = {
classes: {
container: 'dtcc-dropdown',
liner: 'dtcc-dropdown-liner'
},
defaults: {
autoFocus: 'div.dtcc-search input',
className: 'dropdown',
dropdownClass: '',
content: [],
icon: 'menu',
iconActive: '',
text: 'More...'
},
init(config) {
let dt = this.dt();
let dropdown = createElement('div', dropdownContent.classes.container, '', [
createElement('div', dropdownContent.classes.liner)
]);
dropdown._shown = false;
dropdown._close = () => {
dropdown.remove();
dropdown._shown = false;
btn.element().setAttribute('aria-expanded', 'false');
};
dropdown.setAttribute('role', 'dialog');
dropdown.setAttribute('aria-label', dt.i18n('columnControl.dropdown', config.text));
if (config.dropdownClass) {
addClass(dropdown, config.dropdownClass.split(' '));
}
// When FixedHeader is used, the transition between states messes up positioning, so if
// shown we just reattach the dropdown.
dt.on('fixedheader-mode', () => {
if (dropdown._shown) {
attachDropdown(dropdown, dt, config._parents ? config._parents[0] : btn, config.autoFocus);
}
});
// A liner element allows more styling options, so the contents go inside this
let liner = dropdown.childNodes[0];
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.dropdown', config.text))
.icon(config.icon, config.iconActive)
.className(config.className)
.dropdownDisplay(liner)
.handler((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, config.autoFocus);
// When activated using a key - auto focus on the first item in the popover
let focusable = dropdown.querySelector('input, a, button');
if (focusable && e.type === 'keypress') {
focusable.focus();
}
});
btn.element().setAttribute('aria-haspopup', 'dialog');
btn.element().setAttribute('aria-expanded', 'false');
// Add the content for the dropdown
for (let i = 0; i < config.content.length; i++) {
let 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);
let 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', () => {
positionDropdown(dropdown, dt, btn.element());
});
// Focus capture events
let capture = focusCapture(dropdown, btn.element());
document.body.addEventListener('keydown', capture);
dt.on('destroy', () => {
document.body.removeEventListener('keydown', capture);
});
return btn.element();
}
};
// 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>');
}
const 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"/>'),
// Custom
groupAdd: wrap('<path d="M6 21v-7.5m-3.549 3.75H9.75"/><rect width="13.5" height="7.5" x="3" y="3" rx="1.5"/><rect width="7.5" height="7.5" x="13.5" y="13.5" fill="currentColor" rx="1.5"/>'),
// Custom
groupClear: wrap('<rect width="13.5" height="7.5" x="3" y="3" rx="1.5"/><rect width="7.5" height="7.5" x="13.5" y="13.5" rx="1.5"/>'),
// Custom
groupTop: wrap('<rect width="13.5" height="7.5" x="3" y="3" fill="currentColor" rx="1.5"/><rect width="7.5" height="7.5" x="13.5" y="13.5" rx="1.5"/>'),
// Custom
groupRemove: wrap('<path d="M2.451 17.25H9.75"/><rect width="13.5" height="7.5" x="3" y="3" rx="1.5"/><rect width="7.5" height="7.5" x="13.5" y="13.5" rx="1.5"/>'),
// Info
info: wrap('<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>'),
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-tick
searchActive: wrap('<path d="m8 11 2 2 4-4"/><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"/>')
};
let _namespace = 0;
class Button {
active(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
*/
activeList(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
*/
checkDisplay() {
let visible = 0;
let children = this._dom.dropdownDisplay.childNodes;
for (let 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
*/
className(className) {
this._dom.button.classList.add('dtcc-button_' + className);
return this;
}
/**
* Destroy the button, cleaning up event listeners
*/
destroy() {
if (this._s.buttonClick) {
this._dom.button.removeEventListener('click', this._s.buttonClick);
this._dom.button.removeEventListener('keypress', this._s.buttonClick);
}
this._s.host.destroyRemove(this);
}
/**
* 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
*/
dropdownDisplay(el) {
this._dom.dropdownDisplay = el;
return this;
}
/**
* Get the DOM Button element to attach into the document
*
* @returns The Button element
*/
element() {
return this._dom.button;
}
enable(enable) {
if (enable === undefined) {
return this._s.enabled;
}
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
*/
extra(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
*/
handler(fn) {
let buttonClick = (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);
this._s.host.destroyAdd(this);
return this;
}
/**
* Set the icon to display in the button
*
* @param icon Icon name
* @param iconActive Icon to use when in active state
* @returns Button instance
*/
icon(icon, iconActive) {
this._s.icon = icon;
this._s.iconActive = iconActive;
this._checkActive();
return this;
}
text(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;
}
value(val) {
if (val === undefined) {
return this._s.value;
}
this._s.value = val;
return this;
}
/**
* 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.
*/
constructor(dt, host) {
this._s = {
active: false,
activeList: [],
buttonClick: null,
dt: null,
enabled: true,
icon: '',
iconActive: '',
host: null,
label: '',
namespace: '',
value: null
};
this._s.dt = dt;
this._s.host = host;
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.setAttribute('type', 'button');
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);
}
/**
* Check if anything is making this button active
*
* @returns Self for chaining
*/
_checkActive() {
let icon = this._s.icon;
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');
if (this._s.iconActive) {
icon = this._s.iconActive;
}
}
else {
this._dom.state.innerHTML = '';
this._dom.button.classList.remove('dtcc-button_active');
}
if (icon) {
this._dom.icon.innerHTML = icons[icon];
}
return this;
}
}
Button.classes = {
container: 'dtcc-button'
};
class CheckList {
/**
* Add one or more buttons to the list
*
* @param options Configuration for the button(s) to add
* @returns Self for chaining
*/
add(options, update) {
if (!Array.isArray(options)) {
options = [options];
}
for (let i = 0; i < options.length; i++) {
let option = options[i];
let btn = new Button(this._s.dt, this._s.host)
.active(option.active || false)
.handler((e) => {
this._s.handler(e, btn, this._s.buttons, true);
this._updateCount();
})
.icon(option.icon || '')
.text(option.label !== ''
? option.label
: this._s.dt.i18n('columnControl.list.empty', 'Empty'))
.value(option.value);
if (option.label === '') {
btn.className('empty');
}
this._s.buttons.push(btn);
}
let 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
*/
button(val) {
let buttons = this._s.buttons;
for (let 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
*/
clear() {
// Clean up the buttons
for (let 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
*/
element() {
return this._dom.container;
}
/**
* Set the event handler for what happens when a button is clicked
*
* @param fn Event handler
*/
handler(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
*/
searchListener(dt) {
// Column control search clearing (column().columnControl.searchClear() method)
dt.on('cc-search-clear', (e, colIdx) => {
if (colIdx === this._s.host.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
*/
selectAll() {
for (let i = 0; i < this._s.displayed.length; i++) {
this._s.displayed[i].active(true);
}
return this;
}
/**
* Deselect all buttons
*
* @returns Self for chaining
*/
selectNone() {
for (let 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
*/
title(title) {
this._dom.title.innerHTML = title;
return this;
}
values(values) {
let i;
let result = [];
let buttons = this._s.buttons;
if (values !== undefined) {
for (i = 0; i < buttons.length; i++) {
buttons[i].active(values.includes(buttons[i].value()));
}
this._updateCount();
return this;
}
for (i = 0; i < buttons.length; i++) {
if (buttons[i].active()) {
result.push(buttons[i].value());
}
}
return result;
}
/**
* Container for a list of buttons
*/
constructor(dt, host, opts) {
this._s = {
buttons: [],
displayed: [],
dt: null,
handler: () => { },
host: null,
search: ''
};
this._s.dt = dt;
this._s.host = host;
this._dom = {
buttons: createElement('div', 'dtcc-list-buttons'),
container: createElement('div', CheckList.classes.container),
controls: createElement('div', 'dtcc-list-controls'),
empty: createElement('div', 'dtcc-list-empty', dt.i18n('columnControl.list.empty', 'No options')),
title: createElement('div', 'dtcc-list-title'),
selectAll: createElement('button', 'dtcc-list-selectAll', dt.i18n('columnControl.list.all', 'Select')),
selectAllCount: createElement('span'),
selectNone: createElement('button', 'dtcc-list-selectNone', dt.i18n('columnControl.list.none', 'Deselect')),
selectNoneCount: createElement('span'),
search: createElement('input', CheckList.classes.input)
};
let dom = this._dom;
dom.search.setAttribute('type', 'text');
dom.container.append(dom.title);
dom.container.append(dom.controls);
dom.container.append(dom.empty);
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);
dom.selectAll.setAttribute('type', 'button');
dom.selectNone.setAttribute('type', 'button');
}
// Events
let searchInput = () => {
this._s.search = dom.search.value;
this._redraw();
};
let selectAllClick = (e) => {
this.selectAll();
this._s.handler(e, null, this._s.buttons, true);
this._updateCount();
};
let selectNoneClick = (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', () => {
dom.selectAll.removeEventListener('click', selectAllClick);
dom.selectNone.removeEventListener('click', selectNoneClick);
dom.search.removeEventListener('input', searchInput);
});
}
/**
* Update the deselect counter
*/
_updateCount() {
let count = this.values().length;
this._dom.selectNoneCount.innerHTML = count ? '(' + count + ')' : '';
}
/**
* Add the buttons to the page - taking into account filtering
*/
_redraw() {
let buttons = this._s.buttons;
let el = this._dom.buttons;
let searchTerm = this._s.search.toLowerCase();
el.replaceChildren();
this._s.displayed.length = 0;
for (let i = 0; i < buttons.length; i++) {
let btn = buttons[i];
if (!searchTerm ||
btn
.text()
.toLowerCase()
.includes(searchTerm)) {
el.appendChild(btn.element());
this._s.displayed.push(btn);
}
}
this._dom.empty.style.display = buttons.length === 0 ? 'block' : 'none';
el.style.display = buttons.length > 0 ? 'block' : 'none';
this._dom.selectAllCount.innerHTML = searchTerm
? '(' + this._s.displayed.length + ' / ' + buttons.length + ')'
: '(' + buttons.length + ')';
}
}
CheckList.classes = {
container: 'dtcc-list',
input: 'dtcc-list-search'
};
var colVis = {
defaults: {
className: 'colVis',
columns: '',
search: false,
select: false,
title: 'Column visibility'
},
init(config) {
let dt = this.dt();
let checkList = new CheckList(dt, this, {
search: config.search,
select: config.select
})
.title(dt.i18n('columnControl.colVis', config.title))
.handler((e, btn, buttons) => {
if (btn) {
btn.active(!btn.active());
}
apply(buttons);
});
// Need to apply in a loop to allow for select all / select none
let apply = (buttons) => {
for (let i = 0; i < buttons.length; i++) {
let btn = buttons[i];
let idx = btn.value();
let col = dt.column(idx);
if (btn.active() && !col.visible()) {
col.visible(true);
}
else if (!btn.active() && col.visible()) {
col.visible(false);
}
}
};
let rebuild = () => {
let columns = dt.columns(config.columns);
columns.every(function () {
checkList.add({
active: this.visible(),
label: this.title(),
value: this.index()
});
});
};
rebuild();
dt.on('column-visibility', (e, s, colIdx, state) => {
let btn = checkList.button(colIdx);
if (btn) {
btn.active(state);
}
});
dt.on('columns-reordered', (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(config) {
let dt = this.dt();
return {
extend: 'dropdown',
icon: 'columns',
text: dt.i18n('columnControl.colVisDropdown', config.text),
content: [
Object.assign(config, {
extend: 'colVis'
})
]
};
}
};
var info = {
defaults: {
activation: 'hover',
className: 'info',
content: 'attr:title',
contentClass: 'dtcc-popover',
gap: 10,
icon: 'info',
text: ''
},
init(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.order', config.text))
.icon(config.icon)
.className(config.className);
let buttonEl = Dom.s(btn.element());
let popover = Dom.c('div').classAdd(config.contentClass);
let timer;
if (config.content.match(/^attr:/)) {
let name = config.content.replace(/^attr:/, '');
let header = Dom.s(dt.column(this.idx()).header());
let content = header.attr(name);
header.attrRemove(name);
if (!content) {
return;
}
popover.text(content);
}
else {
popover.html(config.content);
}
buttonEl.on(config.activation === 'hover' ? 'mouseenter' : 'click', () => {
show(popover, buttonEl, config.gap);
});
// Hide on exit, but allow a little bit of time for the pointer to get
// into the popover to keep it in place
buttonEl.on('mouseleave', () => {
timer = setTimeout(() => {
remove(popover);
}, 250);
});
popover
.on('mouseenter', () => {
if (timer) {
clearTimeout(timer);
}
})
.on('mouseleave', () => {
remove(popover);
});
return buttonEl[0];
}
};
function show(popover, host, gap) {
// Set to 0 so we can insert, take measurement (reflow) and reposition if
// needed, without a flicker
popover.css('opacity', '0');
host.parent().append(popover);
let height = popover.height('outer');
let width = popover.width('outer');
let buttonWidth = host.width('outer');
let buttonHeight = host.height('outer');
let buttonPosition = host.position();
host.offset();
popover.css({
top: -height - gap + 'px',
left: buttonPosition.left + buttonWidth / 2 - width / 2 + 'px'
});
// Check if overflowing top
if (popover[0].getBoundingClientRect().y < 0) {
popover.css('top', buttonHeight + gap + 'px').classAdd('below');
}
popover.css('opacity', '1');
}
function remove(popover) {
popover.detach().classRemove('below');
}
var order = {
defaults: {
className: 'order',
iconAsc: 'orderAsc',
iconDesc: 'orderDesc',
iconNone: 'orderNone',
statusOnly: false,
text: 'Toggle ordering'
},
init(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.order', config.text))
.icon('orderAsc')
.className(config.className);
if (!config.statusOnly) {
dt.order.listener(btn.element(), () => [this.idx()], () => { });
}
dt.on('order', (e, s, order) => {
let found = order.find((o) => 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(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.orderAddAsc', config.text))
.icon(config.icon)
.className(config.className)
.handler(() => {
let order = dt.order();
order.push([this.idx(), 'asc']);
dt.draw();
});
dt.on('order', (e, s, order) => {
let found = order.some((o) => o.col === this.idx());
btn.enable(!found);
});
return btn.element();
}
};
var orderAddDesc = {
defaults: {
className: 'orderAddDesc',
icon: 'orderAddDesc',
text: 'Add Sort Descending'
},
init(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.orderAddDesc', config.text))
.icon(config.icon)
.className(config.className)
.handler(() => {
let order = dt.order();
order.push([this.idx(), 'desc']);
dt.draw();
});
dt.on('order', (e, s, order) => {
let found = order.some((o) => o.col === this.idx());
btn.enable(!found);
});
return btn.element();
}
};
var orderAsc = {
defaults: {
className: 'orderAsc',
icon: 'orderAsc',
text: 'Sort Ascending'
},
init(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.orderAsc', config.text))
.icon(config.icon)
.className(config.className)
.handler(() => {
this.dt()
.order([
{
idx: this.idx(),
dir: 'asc'
}
])
.draw();
});
dt.on('order', (e, s, order) => {
let found = order.some((o) => o.col === this.idx() && o.dir === 'asc');
btn.active(found);
});
return btn.element();
}
};
var orderClear = {
defaults: {
className: 'orderClear',
icon: 'orderClear',
text: 'Clear sort'
},
init(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.orderClear', config.text))
.icon(config.icon)
.className(config.className)
.handler(() => {
dt.order([]).draw();
});
dt.on('order', (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(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.orderDesc', config.text))
.icon(config.icon)
.className(config.className)
.handler(() => {
this.dt()
.order([
{
idx: this.idx(),
dir: 'desc'
}
])
.draw();
});
dt.on('order', (e, s, order) => {
let found = order.some((o) => 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(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.orderRemove', config.text))
.icon(config.icon)
.className(config.className)
.handler(() => {
// Remove the current column from the ordering array, then reorder the table
let order = dt.order();
let idx = order.findIndex((o) => o[0] === this.idx());
order.splice(idx, 1);
dt.order(order).draw();
});
dt.on('order', (e, s, order) => {
let found = order.some((o) => 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(config) {
return Object.assign(config, { extend: 'order' });
}
};
var reorder = {
defaults: {
className: 'reorder',
icon: 'move',
text: 'Reorder columns'
},
init(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.reorder', config.text))
.icon(config.icon)
.className(config.className);
// The event handling for this is done in ColReorder._addListener - no event
// handler needed here for click / drag
if (this.idx() === 0) {
btn.enable(false);
}
dt.on('columns-reordered', (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(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.reorderLeft', config.text))
.icon(config.icon)
.className(config.className)
.handler(() => {
let 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', (e, details) => {
btn.enable(this.idx() > 0);
});
return btn.element();
}
};
var reorderRight = {
defaults: {
className: 'reorderRight',
icon: 'moveRight',
text: 'Move column right'
},
init(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.reorderRight', config.text))
.icon(config.icon)
.className(config.className)
.handler(() => {
let 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', (e, details) => {
btn.enable(this.idx() < dt.columns().count() - 1);
});
return btn.element();
}
};
/**
* Add an item to the grouping structure
*
* @param dt DataTable API instance
* @param dataSrc Grouping data point to add
* @returns Grouping array
*/
function rowGroupAdd$1(dt, dataSrc) {
let applied = rowGroupApplied(dt);
let idx = applied.indexOf(dataSrc);
if (idx === -1) {
applied.push(dataSrc);
dt.rowGroup().dataSrc(applied);
}
return applied;
}
/**
* Always want an array return
*
* @param dt DataTable API instance
* @returns
*/
function rowGroupApplied(dt) {
let applied = dt.rowGroup().dataSrc();
return Array.isArray(applied)
? applied
: [applied];
}
/**
* Remove all grouping
*
* @param dt DataTable API instance
*/
function rowGroupClear$1(dt) {
dt.rowGroup().dataSrc([]);
}
/**
* Remove an item from the grouping structure
*
* @param dt DataTable API instance
* @param dataSrc Grouping data point to remove
* @returns Grouping array
*/
function rowGroupRemove$1(dt, dataSrc) {
let applied = rowGroupApplied(dt);
let idx = applied.indexOf(dataSrc);
if (idx !== -1) {
applied.splice(idx, 1);
dt.rowGroup().dataSrc(applied);
}
return applied;
}
var rowGroup = {
defaults: {
className: 'rowGroup',
icon: 'groupTop',
order: true,
text: 'Group rows'
},
init(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.rowGroup', config.text))
.icon(config.icon)
.className(config.className)
.handler(() => {
let dataSrc = dt.column(this.idx()).dataSrc();
if (btn.active()) {
// Grouping is active - remove
rowGroupRemove$1(dt, dataSrc);
}
else {
// No grouping by this column yet, set it
rowGroupClear$1(dt);
rowGroupAdd$1(dt, dataSrc);
if (config.order !== false) {
dt.order([this.idx(), 'asc']);
}
}
dt.draw();
});
// Show as active when grouping is applied
dt.on('rowgroup-datasrc', () => {
let applied = rowGroupApplied(dt);
let ours = dt.column(this.idx()).dataSrc();
btn.active(applied.includes(ours));
});
return btn.element();
}
};
var rowGroupAdd = {
defaults: {
className: 'rowGroupAdd',
icon: 'groupAdd',
order: true,
text: 'Add to grouping'
},
init(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.rowGroup', config.text))
.icon(config.icon)
.className(config.className)
.handler(() => {
let dataSrc = dt.column(this.idx()).dataSrc();
if (btn.enable()) {
// No grouping by this column yet, add it
rowGroupAdd$1(dt, dataSrc);
}
dt.draw();
});
// Show as active when grouping is applied
dt.on('rowgroup-datasrc', () => {
let applied = rowGroupApplied(dt);
let ours = dt.column(this.idx()).dataSrc();
btn.enable(!applied.includes(ours));
});
return btn.element();
}
};
var rowGroupClear = {
defaults: {
className: 'rowGroupClear',
icon: 'groupClear',
text: 'Clear all grouping'
},
init(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.rowGroup', config.text))
.icon(config.icon)
.className(config.className)
.handler(() => {
rowGroupClear$1(dt);
dt.draw();
});
// Show as active when any grouping is applied
dt.on('rowgroup-datasrc', () => {
btn.enable(rowGroupApplied(dt).length > 0);
});
// Default status
btn.enable(rowGroupApplied(dt).length > 0);
return btn.element();
}
};
var rowGroupRemove = {
defaults: {
className: 'rowGroupRemove',
icon: 'groupRemove',
order: true,
text: 'Remove from grouping'
},
init(config) {
let dt = this.dt();
let btn = new Button(dt, this)
.text(dt.i18n('columnControl.rowGroup', config.text))
.icon(config.icon)
.className(config.className)
.handler(() => {
let dataSrc = dt.column(this.idx()).dataSrc();
if (btn.enable()) {
// Grouping is active - remove
rowGroupRemove$1(dt, dataSrc);
dt.draw();
}
});
// Show as active when grouping is applied
dt.on('rowgroup-datasrc', () => {
let applied = rowGroupApplied(dt);
let ours = dt.column(this.idx()).dataSrc();
btn.enable(applied.includes(ours));
});
// Default disabled
btn.enable(false);
return btn.element();
}
};
class SearchInput {
/**
* Add a class to the container
*
* @param name Class name to add
* @returns Self for chaining
*/
addClass(name) {
this._dom.container.classList.add(name);
return this;
}
/**
* Clear any applied search
*
* @returns Self for chaining
*/
clear() {
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
*/
clearable(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
*/
element() {
return this._dom.container;
}
/**
* Get the HTML input element for this control
*
* @returns HTML Input element
*/
input() {
return this._dom.input;
}
/**
* Set the list of options for the dropdown
*
* @param opts List of options
* @returns Sel