UNPKG

datatables.net-columncontrol

Version:

ColumnControl for DataTables

1,543 lines (1,518 loc) 112 kB
/*! 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