30) {
// Protect against misconfiguration killing the browser
throw 'Buttons: Too many iterations';
}
}
return Array.isArray(base) ? base : $.extend({}, base);
};
conf = toConfObject(conf);
while (conf && conf.extend) {
// Use `toConfObject` in case the button definition being extended
// is itself a string or a function
if (!_dtButtons[conf.extend]) {
throw 'Cannot extend unknown button type: ' + conf.extend;
}
var objArray = toConfObject(_dtButtons[conf.extend]);
if (Array.isArray(objArray)) {
return objArray;
}
else if (!objArray) {
// This is a little brutal as it might be possible to have a
// valid button without the extend, but if there is no extend
// then the host button would be acting in an undefined state
return false;
}
// Stash the current class name
var originalClassName = objArray.className;
if (conf.config !== undefined && objArray.config !== undefined) {
conf.config = $.extend({}, objArray.config, conf.config);
}
conf = $.extend({}, objArray, conf);
// The extend will have overwritten the original class name if the
// `conf` object also assigned a class, but we want to concatenate
// them so they are list that is combined from all extended buttons
if (originalClassName && conf.className !== originalClassName) {
conf.className = originalClassName + ' ' + conf.className;
}
// Although we want the `conf` object to overwrite almost all of
// the properties of the object being extended, the `extend`
// property should come from the object being extended
conf.extend = objArray.extend;
}
// Buttons to be added to a collection -gives the ability to define
// if buttons should be added to the start or end of a collection
var postfixButtons = conf.postfixButtons;
if (postfixButtons) {
if (!conf.buttons) {
conf.buttons = [];
}
for (i = 0, ien = postfixButtons.length; i < ien; i++) {
conf.buttons.push(postfixButtons[i]);
}
}
var prefixButtons = conf.prefixButtons;
if (prefixButtons) {
if (!conf.buttons) {
conf.buttons = [];
}
for (i = 0, ien = prefixButtons.length; i < ien; i++) {
conf.buttons.splice(i, 0, prefixButtons[i]);
}
}
return conf;
},
/**
* Display (and replace if there is an existing one) a popover attached to a button
* @param {string|node} content Content to show
* @param {DataTable.Api} hostButton DT API instance of the button
* @param {object} inOpts Options (see object below for all options)
*/
_popover: function (content, hostButton, inOpts) {
var dt = hostButton;
var c = this.c;
var closed = false;
var options = $.extend(
{
align: 'button-left', // button-right, dt-container, split-left, split-right
autoClose: false,
background: true,
backgroundClassName: 'dt-button-background',
closeButton: true,
containerClassName: c.dom.collection.container.className,
contentClassName: c.dom.collection.container.content.className,
collectionLayout: '',
collectionTitle: '',
dropup: false,
fade: 400,
popoverTitle: '',
rightAlignClassName: 'dt-button-right',
tag: c.dom.collection.container.tag
},
inOpts
);
var containerSelector =
options.tag + '.' + options.containerClassName.replace(/ /g, '.');
var hostButtonNode = hostButton.node();
var hostNode = options.collectionLayout.includes('fixed') ? $('body') : hostButton.node();
var close = function () {
closed = true;
_fadeOut($(containerSelector), options.fade, function () {
$(this).detach();
});
$(
dt
.buttons('[aria-haspopup="dialog"][aria-expanded="true"]')
.nodes()
).attr('aria-expanded', 'false');
$('div.dt-button-background').off('click.dtb-collection');
Buttons.background(
false,
options.backgroundClassName,
options.fade,
hostNode
);
$(window).off('resize.resize.dtb-collection');
$('body').off('.dtb-collection');
dt.off('buttons-action.b-internal');
dt.off('destroy');
$('body').trigger('buttons-popover-hide.dt');
};
if (content === false) {
close();
return;
}
var existingExpanded = $(
dt.buttons('[aria-haspopup="dialog"][aria-expanded="true"]').nodes()
);
if (existingExpanded.length) {
// Reuse the current position if the button that was triggered is inside an existing collection
if (hostNode.closest(containerSelector).length) {
hostNode = existingExpanded.eq(0);
}
close();
}
// Sort buttons if defined
if (options.sort) {
var elements = $('button', content)
.map(function (idx, el) {
return {
text: $(el).text(),
el: el
};
})
.toArray();
elements.sort(function (a, b) {
return a.text.localeCompare(b.text);
});
$(content).append(elements.map(function (v) {
return v.el;
}));
}
// Try to be smart about the layout
var cnt = $('.dt-button', content).length;
var mod = '';
if (cnt === 3) {
mod = 'dtb-b3';
}
else if (cnt === 2) {
mod = 'dtb-b2';
}
else if (cnt === 1) {
mod = 'dtb-b1';
}
var display = $('<' + options.tag + '/>')
.addClass(options.containerClassName)
.addClass(options.collectionLayout)
.addClass(options.splitAlignClass)
.addClass(mod)
.css('display', 'none')
.attr({
'aria-modal': true,
role: 'dialog'
});
content = $(content)
.addClass(options.contentClassName)
.attr('role', 'menu')
.appendTo(display);
hostButtonNode.attr('aria-expanded', 'true');
if (hostNode.parents('body')[0] !== document.body) {
hostNode = $(document.body).children('div, section, p').last();
}
if (options.popoverTitle) {
display.prepend(
'' +
options.popoverTitle +
'
'
);
}
else if (options.collectionTitle) {
display.prepend(
'' +
options.collectionTitle +
'
'
);
}
if (options.closeButton) {
display
.prepend('×
')
.addClass('dtb-collection-closeable');
}
_fadeIn(display.insertAfter(hostNode), options.fade);
var tableContainer = $(hostButton.table().container());
var position = display.css('position');
if (options.span === 'container' || options.align === 'dt-container') {
hostNode = hostNode.parent();
display.css('width', tableContainer.width());
}
// Align the popover relative to the DataTables container
// Useful for wide popovers such as SearchPanes
if (position === 'absolute') {
// Align relative to the host button
var offsetParent = $(hostNode[0].offsetParent);
var buttonPosition = hostNode.position();
var buttonOffset = hostNode.offset();
var tableSizes = offsetParent.offset();
var containerPosition = offsetParent.position();
var computed = window.getComputedStyle(offsetParent[0]);
tableSizes.height = offsetParent.outerHeight();
tableSizes.width =
offsetParent.width() + parseFloat(computed.paddingLeft);
tableSizes.right = tableSizes.left + tableSizes.width;
tableSizes.bottom = tableSizes.top + tableSizes.height;
// Set the initial position so we can read height / width
var top = buttonPosition.top + hostNode.outerHeight();
var left = buttonPosition.left;
display.css({
top: top,
left: left
});
// Get the popover position
computed = window.getComputedStyle(display[0]);
var popoverSizes = display.offset();
popoverSizes.height = display.outerHeight();
popoverSizes.width = display.outerWidth();
popoverSizes.right = popoverSizes.left + popoverSizes.width;
popoverSizes.bottom = popoverSizes.top + popoverSizes.height;
popoverSizes.marginTop = parseFloat(computed.marginTop);
popoverSizes.marginBottom = parseFloat(computed.marginBottom);
// First position per the class requirements - pop up and right align
if (options.dropup) {
top =
buttonPosition.top -
popoverSizes.height -
popoverSizes.marginTop -
popoverSizes.marginBottom;
}
if (
options.align === 'button-right' ||
display.hasClass(options.rightAlignClassName)
) {
left =
buttonPosition.left -
popoverSizes.width +
hostNode.outerWidth();
}
// Container alignment - make sure it doesn't overflow the table container
if (
options.align === 'dt-container' ||
options.align === 'container'
) {
if (left < buttonPosition.left) {
left = -buttonPosition.left;
}
}
// Window adjustment
if (
containerPosition.left + left + popoverSizes.width >
$(window).width()
) {
// Overflowing the document to the right
left =
$(window).width() -
popoverSizes.width -
containerPosition.left;
}
if (buttonOffset.left + left < 0) {
// Off to the left of the document
left = -buttonOffset.left;
}
if (
containerPosition.top + top + popoverSizes.height >
$(window).height() + $(window).scrollTop()
) {
// Pop up if otherwise we'd need the user to scroll down
top =
buttonPosition.top -
popoverSizes.height -
popoverSizes.marginTop -
popoverSizes.marginBottom;
}
if (offsetParent.offset().top + top < $(window).scrollTop()) {
// Correction for when the top is beyond the top of the page
top = buttonPosition.top + hostNode.outerHeight();
}
// Calculations all done - now set it
display.css({
top: top,
left: left
});
}
else {
// Fix position - centre on screen
var place = function () {
var half = $(window).height() / 2;
var top = display.height() / 2;
if (top > half) {
top = half;
}
display.css('marginTop', top * -1);
};
place();
$(window).on('resize.dtb-collection', function () {
place();
});
}
if (options.background) {
Buttons.background(
true,
options.backgroundClassName,
options.fade,
options.backgroundHost || hostNode
);
}
// This is bonkers, but if we don't have a click listener on the
// background element, iOS Safari will ignore the body click
// listener below. An empty function here is all that is
// required to make it work...
$('div.dt-button-background').on(
'click.dtb-collection',
function () {}
);
if (options.autoClose) {
setTimeout(function () {
dt.on('buttons-action.b-internal', function (e, btn, dt, node) {
if (node[0] === hostNode[0]) {
return;
}
close();
});
}, 0);
}
$(display).trigger('buttons-popover.dt');
dt.on('destroy', close);
setTimeout(function () {
closed = false;
$('body')
.on('click.dtb-collection', function (e) {
if (closed) {
return;
}
// andSelf is deprecated in jQ1.8, but we want 1.7 compat
var back = $.fn.addBack ? 'addBack' : 'andSelf';
var parent = $(e.target).parent()[0];
if (
(!$(e.target).parents()[back]().filter(content)
.length &&
!$(parent).hasClass('dt-buttons')) ||
$(e.target).hasClass('dt-button-background')
) {
close();
}
})
.on('keyup.dtb-collection', function (e) {
if (e.keyCode === 27) {
close();
}
})
.on('keydown.dtb-collection', function (e) {
// Focus trap for tab key
var elements = $('a, button', content);
var active = document.activeElement;
if (e.keyCode !== 9) {
// tab
return;
}
if (elements.index(active) === -1) {
// If current focus is not inside the popover
elements.first().focus();
e.preventDefault();
}
else if (e.shiftKey) {
// Reverse tabbing order when shift key is pressed
if (active === elements[0]) {
elements.last().focus();
e.preventDefault();
}
}
else {
if (active === elements.last()[0]) {
elements.first().focus();
e.preventDefault();
}
}
});
}, 0);
}
});
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Statics
*/
/**
* Show / hide a background layer behind a collection
* @param {boolean} Flag to indicate if the background should be shown or
* hidden
* @param {string} Class to assign to the background
* @static
*/
Buttons.background = function (show, className, fade, insertPoint) {
if (fade === undefined) {
fade = 400;
}
if (!insertPoint) {
insertPoint = document.body;
}
if (show) {
_fadeIn(
$('')
.addClass(className)
.css('display', 'none')
.insertAfter(insertPoint),
fade
);
}
else {
_fadeOut($('div.' + className), fade, function () {
$(this).removeClass(className).remove();
});
}
};
/**
* Instance selector - select Buttons instances based on an instance selector
* value from the buttons assigned to a DataTable. This is only useful if
* multiple instances are attached to a DataTable.
* @param {string|int|array} Instance selector - see `instance-selector`
* documentation on the DataTables site
* @param {array} Button instance array that was attached to the DataTables
* settings object
* @return {array} Buttons instances
* @static
*/
Buttons.instanceSelector = function (group, buttons) {
if (group === undefined || group === null) {
return $.map(buttons, function (v) {
return v.inst;
});
}
var ret = [];
var names = $.map(buttons, function (v) {
return v.name;
});
// Flatten the group selector into an array of single options
var process = function (input) {
if (Array.isArray(input)) {
for (var i = 0, ien = input.length; i < ien; i++) {
process(input[i]);
}
return;
}
if (typeof input === 'string') {
if (input.indexOf(',') !== -1) {
// String selector, list of names
process(input.split(','));
}
else {
// String selector individual name
var idx = $.inArray(input.trim(), names);
if (idx !== -1) {
ret.push(buttons[idx].inst);
}
}
}
else if (typeof input === 'number') {
// Index selector
ret.push(buttons[input].inst);
}
else if (typeof input === 'object' && input.nodeName) {
// Element selector
for (var j = 0; j < buttons.length; j++) {
if (buttons[j].inst.dom.container[0] === input) {
ret.push(buttons[j].inst);
}
}
}
else if (typeof input === 'object') {
// Actual instance selector
ret.push(input);
}
};
process(group);
return ret;
};
/**
* Button selector - select one or more buttons from a selector input so some
* operation can be performed on them.
* @param {array} Button instances array that the selector should operate on
* @param {string|int|node|jQuery|array} Button selector - see
* `button-selector` documentation on the DataTables site
* @return {array} Array of objects containing `inst` and `idx` properties of
* the selected buttons so you know which instance each button belongs to.
* @static
*/
Buttons.buttonSelector = function (insts, selector) {
var ret = [];
var nodeBuilder = function (a, buttons, baseIdx) {
var button;
var idx;
for (var i = 0, ien = buttons.length; i < ien; i++) {
button = buttons[i];
if (button) {
idx = baseIdx !== undefined ? baseIdx + i : i + '';
a.push({
node: button.node,
name: button.conf.name,
idx: idx
});
if (button.buttons) {
nodeBuilder(a, button.buttons, idx + '-');
}
}
}
};
var run = function (selector, inst) {
var i, ien;
var buttons = [];
nodeBuilder(buttons, inst.s.buttons);
var nodes = $.map(buttons, function (v) {
return v.node;
});
if (Array.isArray(selector) || selector instanceof $) {
for (i = 0, ien = selector.length; i < ien; i++) {
run(selector[i], inst);
}
return;
}
if (selector === null || selector === undefined || selector === '*') {
// Select all
for (i = 0, ien = buttons.length; i < ien; i++) {
ret.push({
inst: inst,
node: buttons[i].node
});
}
}
else if (typeof selector === 'number') {
// Main button index selector
if (inst.s.buttons[selector]) {
ret.push({
inst: inst,
node: inst.s.buttons[selector].node
});
}
}
else if (typeof selector === 'string') {
if (selector.indexOf(',') !== -1) {
// Split
var a = selector.split(',');
for (i = 0, ien = a.length; i < ien; i++) {
run(a[i].trim(), inst);
}
}
else if (selector.match(/^\d+(\-\d+)*$/)) {
// Sub-button index selector
var indexes = $.map(buttons, function (v) {
return v.idx;
});
ret.push({
inst: inst,
node: buttons[$.inArray(selector, indexes)].node
});
}
else if (selector.indexOf(':name') !== -1) {
// Button name selector
var name = selector.replace(':name', '');
for (i = 0, ien = buttons.length; i < ien; i++) {
if (buttons[i].name === name) {
ret.push({
inst: inst,
node: buttons[i].node
});
}
}
}
else {
// jQuery selector on the nodes
$(nodes)
.filter(selector)
.each(function () {
ret.push({
inst: inst,
node: this
});
});
}
}
else if (typeof selector === 'object' && selector.nodeName) {
// Node selector
var idx = $.inArray(selector, nodes);
if (idx !== -1) {
ret.push({
inst: inst,
node: nodes[idx]
});
}
}
};
for (var i = 0, ien = insts.length; i < ien; i++) {
var inst = insts[i];
run(selector, inst);
}
return ret;
};
/**
* Default function used for formatting output data.
* @param {*} str Data to strip
*/
Buttons.stripData = function (str, config) {
// If the input is an HTML element, we can use the HTML from it (HTML might be stripped below).
if (typeof str === 'object' && str.nodeName && str.nodeType) {
str = str.innerHTML;
}
if (typeof str !== 'string') {
return str;
}
// Always remove script tags
str = Buttons.stripHtmlScript(str);
// Always remove comments
str = Buttons.stripHtmlComments(str);
if (!config || config.stripHtml) {
str = DataTable.util.stripHtml(str);
}
if (!config || config.trim) {
str = str.trim();
}
if (!config || config.stripNewlines) {
str = str.replace(/\n/g, ' ');
}
if (!config || config.decodeEntities) {
if (_entityDecoder) {
str = _entityDecoder(str);
}
else {
_exportTextarea.innerHTML = str;
str = _exportTextarea.value;
}
}
// Prevent Excel from running a formula
if (!config || config.escapeExcelFormula) {
if (str.match(/^[=+\-@\t\r]/)) {
console.log('matching and updateing');
str = "'" + str;
}
}
return str;
};
/**
* Provide a custom entity decoding function - e.g. a regex one, which can be
* much faster than the built in DOM option, but also larger code size.
* @param {function} fn
*/
Buttons.entityDecoder = function (fn) {
_entityDecoder = fn;
};
/**
* Common function for stripping HTML comments
*
* @param {*} input
* @returns
*/
Buttons.stripHtmlComments = function (input) {
var previous;
do {
previous = input;
input = input.replace(/(