/*! Buttons for DataTables 2.4.1 * © SpryMedia Ltd - datatables.net/license */ (function( factory ){ if ( typeof define === 'function' && define.amd ) { // AMD define( ['jquery', 'datatables.net'], function ( $ ) { return factory( $, window, document ); } ); } else if ( typeof exports === 'object' ) { // CommonJS var jq = require('jquery'); var cjsRequires = function (root, $) { if ( ! $.fn.dataTable ) { require('datatables.net')(root, $); } }; if (typeof window === 'undefined') { module.exports = function (root, $) { if ( ! root ) { // CommonJS environments without a window global must pass a // root. This will give an error otherwise root = window; } if ( ! $ ) { $ = jq( root ); } cjsRequires( root, $ ); return factory( $, root, root.document ); }; } else { cjsRequires( window, jq ); module.exports = factory( jq, window, window.document ); } } else { // Browser factory( jQuery, window, document ); } }(function( $, window, document, undefined ) { 'use strict'; var DataTable = $.fn.dataTable; // Used for namespacing events added to the document by each instance, so they // can be removed on destroy var _instCounter = 0; // Button namespacing counter for namespacing events on individual buttons var _buttonCounter = 0; var _dtButtons = DataTable.ext.buttons; // Allow for jQuery slim function _fadeIn(el, duration, fn) { if ($.fn.animate) { el.stop().fadeIn(duration, fn); } else { el.css('display', 'block'); if (fn) { fn.call(el); } } } function _fadeOut(el, duration, fn) { if ($.fn.animate) { el.stop().fadeOut(duration, fn); } else { el.css('display', 'none'); if (fn) { fn.call(el); } } } /** * [Buttons description] * @param {[type]} * @param {[type]} */ var Buttons = function (dt, config) { // If not created with a `new` keyword then we return a wrapper function that // will take the settings object for a DT. This allows easy use of new instances // with the `layout` option - e.g. `topLeft: $.fn.dataTable.Buttons( ... )`. if (!(this instanceof Buttons)) { return function (settings) { return new Buttons(settings, dt).container(); }; } // If there is no config set it to an empty object if (typeof config === 'undefined') { config = {}; } // Allow a boolean true for defaults if (config === true) { config = {}; } // For easy configuration of buttons an array can be given if (Array.isArray(config)) { config = { buttons: config }; } this.c = $.extend(true, {}, Buttons.defaults, config); // Don't want a deep copy for the buttons if (config.buttons) { this.c.buttons = config.buttons; } this.s = { dt: new DataTable.Api(dt), buttons: [], listenKeys: '', namespace: 'dtb' + _instCounter++ }; this.dom = { container: $('<' + this.c.dom.container.tag + '/>').addClass(this.c.dom.container.className) }; this._constructor(); }; $.extend(Buttons.prototype, { /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Public methods */ /** * Get the action of a button * @param {int|string} Button index * @return {function} */ /** * Set the action of a button * @param {node} node Button element * @param {function} action Function to set * @return {Buttons} Self for chaining */ action: function (node, action) { var button = this._nodeToButton(node); if (action === undefined) { return button.conf.action; } button.conf.action = action; return this; }, /** * Add an active class to the button to make to look active or get current * active state. * @param {node} node Button element * @param {boolean} [flag] Enable / disable flag * @return {Buttons} Self for chaining or boolean for getter */ active: function (node, flag) { var button = this._nodeToButton(node); var klass = this.c.dom.button.active; var jqNode = $(button.node); if ( button.inCollection && this.c.dom.collection.button && this.c.dom.collection.button.active !== undefined ) { klass = this.c.dom.collection.button.active; } if (flag === undefined) { return jqNode.hasClass(klass); } jqNode.toggleClass(klass, flag === undefined ? true : flag); return this; }, /** * Add a new button * @param {object} config Button configuration object, base string name or function * @param {int|string} [idx] Button index for where to insert the button * @param {boolean} [draw=true] Trigger a draw. Set a false when adding * lots of buttons, until the last button. * @return {Buttons} Self for chaining */ add: function (config, idx, draw) { var buttons = this.s.buttons; if (typeof idx === 'string') { var split = idx.split('-'); var base = this.s; for (var i = 0, ien = split.length - 1; i < ien; i++) { base = base.buttons[split[i] * 1]; } buttons = base.buttons; idx = split[split.length - 1] * 1; } this._expandButton( buttons, config, config !== undefined ? config.split : undefined, (config === undefined || config.split === undefined || config.split.length === 0) && base !== undefined, false, idx ); if (draw === undefined || draw === true) { this._draw(); } return this; }, /** * Clear buttons from a collection and then insert new buttons */ collectionRebuild: function (node, newButtons) { var button = this._nodeToButton(node); if (newButtons !== undefined) { var i; // Need to reverse the array for (i = button.buttons.length - 1; i >= 0; i--) { this.remove(button.buttons[i].node); } // If the collection has prefix and / or postfix buttons we need to add them in if (button.conf.prefixButtons) { newButtons.unshift.apply(newButtons, button.conf.prefixButtons); } if (button.conf.postfixButtons) { newButtons.push.apply(newButtons, button.conf.postfixButtons); } for (i = 0; i < newButtons.length; i++) { var newBtn = newButtons[i]; this._expandButton( button.buttons, newBtn, newBtn !== undefined && newBtn.config !== undefined && newBtn.config.split !== undefined, true, newBtn.parentConf !== undefined && newBtn.parentConf.split !== undefined, null, newBtn.parentConf ); } } this._draw(button.collection, button.buttons); }, /** * Get the container node for the buttons * @return {jQuery} Buttons node */ container: function () { return this.dom.container; }, /** * Disable a button * @param {node} node Button node * @return {Buttons} Self for chaining */ disable: function (node) { var button = this._nodeToButton(node); $(button.node).addClass(this.c.dom.button.disabled).prop('disabled', true); return this; }, /** * Destroy the instance, cleaning up event handlers and removing DOM * elements * @return {Buttons} Self for chaining */ destroy: function () { // Key event listener $('body').off('keyup.' + this.s.namespace); // Individual button destroy (so they can remove their own events if // needed). Take a copy as the array is modified by `remove` var buttons = this.s.buttons.slice(); var i, ien; for (i = 0, ien = buttons.length; i < ien; i++) { this.remove(buttons[i].node); } // Container this.dom.container.remove(); // Remove from the settings object collection var buttonInsts = this.s.dt.settings()[0]; for (i = 0, ien = buttonInsts.length; i < ien; i++) { if (buttonInsts.inst === this) { buttonInsts.splice(i, 1); break; } } return this; }, /** * Enable / disable a button * @param {node} node Button node * @param {boolean} [flag=true] Enable / disable flag * @return {Buttons} Self for chaining */ enable: function (node, flag) { if (flag === false) { return this.disable(node); } var button = this._nodeToButton(node); $(button.node).removeClass(this.c.dom.button.disabled).prop('disabled', false); return this; }, /** * Get a button's index * * This is internally recursive * @param {element} node Button to get the index of * @return {string} Button index */ index: function (node, nested, buttons) { if (!nested) { nested = ''; buttons = this.s.buttons; } for (var i = 0, ien = buttons.length; i < ien; i++) { var inner = buttons[i].buttons; if (buttons[i].node === node) { return nested + i; } if (inner && inner.length) { var match = this.index(node, i + '-', inner); if (match !== null) { return match; } } } return null; }, /** * Get the instance name for the button set selector * @return {string} Instance name */ name: function () { return this.c.name; }, /** * Get a button's node of the buttons container if no button is given * @param {node} [node] Button node * @return {jQuery} Button element, or container */ node: function (node) { if (!node) { return this.dom.container; } var button = this._nodeToButton(node); return $(button.node); }, /** * Set / get a processing class on the selected button * @param {element} node Triggering button node * @param {boolean} flag true to add, false to remove, undefined to get * @return {boolean|Buttons} Getter value or this if a setter. */ processing: function (node, flag) { var dt = this.s.dt; var button = this._nodeToButton(node); if (flag === undefined) { return $(button.node).hasClass('processing'); } $(button.node).toggleClass('processing', flag); $(dt.table().node()).triggerHandler('buttons-processing.dt', [ flag, dt.button(node), dt, $(node), button.conf ]); return this; }, /** * Remove a button. * @param {node} node Button node * @return {Buttons} Self for chaining */ remove: function (node) { var button = this._nodeToButton(node); var host = this._nodeToHost(node); var dt = this.s.dt; // Remove any child buttons first if (button.buttons.length) { for (var i = button.buttons.length - 1; i >= 0; i--) { this.remove(button.buttons[i].node); } } button.conf.destroying = true; // Allow the button to remove event handlers, etc if (button.conf.destroy) { button.conf.destroy.call(dt.button(node), dt, $(node), button.conf); } this._removeKey(button.conf); $(button.node).remove(); var idx = $.inArray(button, host); host.splice(idx, 1); return this; }, /** * Get the text for a button * @param {int|string} node Button index * @return {string} Button text */ /** * Set the text for a button * @param {int|string|function} node Button index * @param {string} label Text * @return {Buttons} Self for chaining */ text: function (node, label) { var button = this._nodeToButton(node); var textNode = button.textNode; var dt = this.s.dt; var jqNode = $(button.node); var text = function (opt) { return typeof opt === 'function' ? opt(dt, jqNode, button.conf) : opt; }; if (label === undefined) { return text(button.conf.text); } button.conf.text = label; textNode.html(text(label)); return this; }, /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Constructor */ /** * Buttons constructor * @private */ _constructor: function () { var that = this; var dt = this.s.dt; var dtSettings = dt.settings()[0]; var buttons = this.c.buttons; if (!dtSettings._buttons) { dtSettings._buttons = []; } dtSettings._buttons.push({ inst: this, name: this.c.name }); for (var i = 0, ien = buttons.length; i < ien; i++) { this.add(buttons[i]); } dt.on('destroy', function (e, settings) { if (settings === dtSettings) { that.destroy(); } }); // Global key event binding to listen for button keys $('body').on('keyup.' + this.s.namespace, function (e) { if (!document.activeElement || document.activeElement === document.body) { // SUse a string of characters for fast lookup of if we need to // handle this var character = String.fromCharCode(e.keyCode).toLowerCase(); if (that.s.listenKeys.toLowerCase().indexOf(character) !== -1) { that._keypress(character, e); } } }); }, /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Private methods */ /** * Add a new button to the key press listener * @param {object} conf Resolved button configuration object * @private */ _addKey: function (conf) { if (conf.key) { this.s.listenKeys += $.isPlainObject(conf.key) ? conf.key.key : conf.key; } }, /** * Insert the buttons into the container. Call without parameters! * @param {node} [container] Recursive only - Insert point * @param {array} [buttons] Recursive only - Buttons array * @private */ _draw: function (container, buttons) { if (!container) { container = this.dom.container; buttons = this.s.buttons; } container.children().detach(); for (var i = 0, ien = buttons.length; i < ien; i++) { container.append(buttons[i].inserter); container.append(' '); if (buttons[i].buttons && buttons[i].buttons.length) { this._draw(buttons[i].collection, buttons[i].buttons); } } }, /** * Create buttons from an array of buttons * @param {array} attachTo Buttons array to attach to * @param {object} button Button definition * @param {boolean} inCollection true if the button is in a collection * @private */ _expandButton: function ( attachTo, button, split, inCollection, inSplit, attachPoint, parentConf ) { var dt = this.s.dt; var isSplit = false; var domCollection = this.c.dom.collection; var buttons = !Array.isArray(button) ? [button] : button; if (button === undefined) { buttons = !Array.isArray(split) ? [split] : split; } for (var i = 0, ien = buttons.length; i < ien; i++) { var conf = this._resolveExtends(buttons[i]); if (!conf) { continue; } isSplit = conf.config && conf.config.split ? true : false; // If the configuration is an array, then expand the buttons at this // point if (Array.isArray(conf)) { this._expandButton( attachTo, conf, built !== undefined && built.conf !== undefined ? built.conf.split : undefined, inCollection, parentConf !== undefined && parentConf.split !== undefined, attachPoint, parentConf ); continue; } var built = this._buildButton( conf, inCollection, conf.split !== undefined || (conf.config !== undefined && conf.config.split !== undefined), inSplit ); if (!built) { continue; } if (attachPoint !== undefined && attachPoint !== null) { attachTo.splice(attachPoint, 0, built); attachPoint++; } else { attachTo.push(built); } // Create the dropdown for a collection if (built.conf.buttons) { built.collection = $('<' + domCollection.container.content.tag + '/>'); built.conf._collection = built.collection; $(built.node).append(domCollection.action.dropHtml); this._expandButton( built.buttons, built.conf.buttons, built.conf.split, !isSplit, isSplit, attachPoint, built.conf ); } // And the split collection if (built.conf.split) { built.collection = $('<' + domCollection.container.tag + '/>'); built.conf._collection = built.collection; for (var j = 0; j < built.conf.split.length; j++) { var item = built.conf.split[j]; if (typeof item === 'object') { item.parent = parentConf; if (item.collectionLayout === undefined) { item.collectionLayout = built.conf.collectionLayout; } if (item.dropup === undefined) { item.dropup = built.conf.dropup; } if (item.fade === undefined) { item.fade = built.conf.fade; } } } this._expandButton( built.buttons, built.conf.buttons, built.conf.split, !isSplit, isSplit, attachPoint, built.conf ); } built.conf.parent = parentConf; // init call is made here, rather than buildButton as it needs to // be selectable, and for that it needs to be in the buttons array if (conf.init) { conf.init.call(dt.button(built.node), dt, $(built.node), conf); } } }, /** * Create an individual button * @param {object} config Resolved button configuration * @param {boolean} inCollection `true` if a collection button * @return {object} Completed button description object * @private */ _buildButton: function (config, inCollection, isSplit, inSplit) { var configDom = this.c.dom; var textNode; var dt = this.s.dt; var text = function (opt) { return typeof opt === 'function' ? opt(dt, button, config) : opt; }; // Create an object that describes the button which can be in `dom.button`, or // `dom.collection.button` or `dom.split.button` or `dom.collection.split.button`! // Each should extend from `dom.button`. var dom = $.extend(true, {}, configDom.button); if (inCollection && isSplit && configDom.collection.split) { $.extend(true, dom, configDom.collection.split.action); } else if (inSplit || inCollection) { $.extend(true, dom, configDom.collection.button); } else if (isSplit) { $.extend(true, dom, configDom.split.button); } // Spacers don't do much other than insert an element into the DOM if (config.spacer) { var spacer = $('<' + dom.spacer.tag + '/>') .addClass('dt-button-spacer ' + config.style + ' ' + dom.spacer.className) .html(text(config.text)); return { conf: config, node: spacer, inserter: spacer, buttons: [], inCollection: inCollection, isSplit: isSplit, collection: null, textNode: spacer }; } // Make sure that the button is available based on whatever requirements // it has. For example, PDF button require pdfmake if (config.available && !config.available(dt, config) && !config.hasOwnProperty('html')) { return false; } var button; if (!config.hasOwnProperty('html')) { var action = function (e, dt, button, config) { config.action.call(dt.button(button), e, dt, button, config); $(dt.table().node()).triggerHandler('buttons-action.dt', [ dt.button(button), dt, button, config ]); }; var tag = config.tag || dom.tag; var clickBlurs = config.clickBlurs === undefined ? true : config.clickBlurs; button = $('<' + tag + '/>') .addClass(dom.className) .attr('tabindex', this.s.dt.settings()[0].iTabIndex) .attr('aria-controls', this.s.dt.table().node().id) .on('click.dtb', function (e) { e.preventDefault(); if (!button.hasClass(dom.disabled) && config.action) { action(e, dt, button, config); } if (clickBlurs) { button.trigger('blur'); } }) .on('keypress.dtb', function (e) { if (e.keyCode === 13) { e.preventDefault(); if (!button.hasClass(dom.disabled) && config.action) { action(e, dt, button, config); } } }); // Make `a` tags act like a link if (tag.toLowerCase() === 'a') { button.attr('href', '#'); } // Button tags should have `type=button` so they don't have any default behaviour if (tag.toLowerCase() === 'button') { button.attr('type', 'button'); } if (dom.liner.tag) { var liner = $('<' + dom.liner.tag + '/>') .html(text(config.text)) .addClass(dom.liner.className); if (dom.liner.tag.toLowerCase() === 'a') { liner.attr('href', '#'); } button.append(liner); textNode = liner; } else { button.html(text(config.text)); textNode = button; } if (config.enabled === false) { button.addClass(dom.disabled); } if (config.className) { button.addClass(config.className); } if (config.titleAttr) { button.attr('title', text(config.titleAttr)); } if (config.attr) { button.attr(config.attr); } if (!config.namespace) { config.namespace = '.dt-button-' + _buttonCounter++; } if (config.config !== undefined && config.config.split) { config.split = config.config.split; } } else { button = $(config.html); } var buttonContainer = this.c.dom.buttonContainer; var inserter; if (buttonContainer && buttonContainer.tag) { inserter = $('<' + buttonContainer.tag + '/>') .addClass(buttonContainer.className) .append(button); } else { inserter = button; } this._addKey(config); // Style integration callback for DOM manipulation // Note that this is _not_ documented. It is currently // for style integration only if (this.c.buttonCreated) { inserter = this.c.buttonCreated(config, inserter); } var splitDiv; if (isSplit) { var dropdownConf = inCollection ? $.extend(true, this.c.dom.split, this.c.dom.collection.split) : this.c.dom.split; var wrapperConf = dropdownConf.wrapper; splitDiv = $('<' + wrapperConf.tag + '/>') .addClass(wrapperConf.className) .append(button); var dropButtonConfig = $.extend(config, { align: dropdownConf.dropdown.align, attr: { 'aria-haspopup': 'dialog', 'aria-expanded': false }, className: dropdownConf.dropdown.className, closeButton: false, splitAlignClass: dropdownConf.dropdown.splitAlignClass, text: dropdownConf.dropdown.text }); this._addKey(dropButtonConfig); var splitAction = function (e, dt, button, config) { _dtButtons.split.action.call(dt.button(splitDiv), e, dt, button, config); $(dt.table().node()).triggerHandler('buttons-action.dt', [ dt.button(button), dt, button, config ]); button.attr('aria-expanded', true); }; var dropButton = $( '' ) .html(dropdownConf.dropdown.dropHtml) .on('click.dtb', function (e) { e.preventDefault(); e.stopPropagation(); if (!dropButton.hasClass(dom.disabled)) { splitAction(e, dt, dropButton, dropButtonConfig); } if (clickBlurs) { dropButton.trigger('blur'); } }) .on('keypress.dtb', function (e) { if (e.keyCode === 13) { e.preventDefault(); if (!dropButton.hasClass(dom.disabled)) { splitAction(e, dt, dropButton, dropButtonConfig); } } }); if (config.split.length === 0) { dropButton.addClass('dtb-hide-drop'); } splitDiv.append(dropButton).attr(dropButtonConfig.attr); } return { conf: config, node: isSplit ? splitDiv.get(0) : button.get(0), inserter: isSplit ? splitDiv : inserter, buttons: [], inCollection: inCollection, isSplit: isSplit, inSplit: inSplit, collection: null, textNode: textNode }; }, /** * Get the button object from a node (recursive) * @param {node} node Button node * @param {array} [buttons] Button array, uses base if not defined * @return {object} Button object * @private */ _nodeToButton: function (node, buttons) { if (!buttons) { buttons = this.s.buttons; } for (var i = 0, ien = buttons.length; i < ien; i++) { if (buttons[i].node === node) { return buttons[i]; } if (buttons[i].buttons.length) { var ret = this._nodeToButton(node, buttons[i].buttons); if (ret) { return ret; } } } }, /** * Get container array for a button from a button node (recursive) * @param {node} node Button node * @param {array} [buttons] Button array, uses base if not defined * @return {array} Button's host array * @private */ _nodeToHost: function (node, buttons) { if (!buttons) { buttons = this.s.buttons; } for (var i = 0, ien = buttons.length; i < ien; i++) { if (buttons[i].node === node) { return buttons; } if (buttons[i].buttons.length) { var ret = this._nodeToHost(node, buttons[i].buttons); if (ret) { return ret; } } } }, /** * Handle a key press - determine if any button's key configured matches * what was typed and trigger the action if so. * @param {string} character The character pressed * @param {object} e Key event that triggered this call * @private */ _keypress: function (character, e) { // Check if this button press already activated on another instance of Buttons if (e._buttonsHandled) { return; } var run = function (conf, node) { if (!conf.key) { return; } if (conf.key === character) { e._buttonsHandled = true; $(node).click(); } else if ($.isPlainObject(conf.key)) { if (conf.key.key !== character) { return; } if (conf.key.shiftKey && !e.shiftKey) { return; } if (conf.key.altKey && !e.altKey) { return; } if (conf.key.ctrlKey && !e.ctrlKey) { return; } if (conf.key.metaKey && !e.metaKey) { return; } // Made it this far - it is good e._buttonsHandled = true; $(node).click(); } }; var recurse = function (a) { for (var i = 0, ien = a.length; i < ien; i++) { run(a[i].conf, a[i].node); if (a[i].buttons.length) { recurse(a[i].buttons); } } }; recurse(this.s.buttons); }, /** * Remove a key from the key listener for this instance (to be used when a * button is removed) * @param {object} conf Button configuration * @private */ _removeKey: function (conf) { if (conf.key) { var character = $.isPlainObject(conf.key) ? conf.key.key : conf.key; // Remove only one character, as multiple buttons could have the // same listening key var a = this.s.listenKeys.split(''); var idx = $.inArray(character, a); a.splice(idx, 1); this.s.listenKeys = a.join(''); } }, /** * Resolve a button configuration * @param {string|function|object} conf Button config to resolve * @return {object} Button configuration * @private */ _resolveExtends: function (conf) { var that = this; var dt = this.s.dt; var i, ien; var toConfObject = function (base) { var loop = 0; // Loop until we have resolved to a button configuration, or an // array of button configurations (which will be iterated // separately) while (!$.isPlainObject(base) && !Array.isArray(base)) { if (base === undefined) { return; } if (typeof base === 'function') { base = base.call(that, dt, conf); if (!base) { return false; } } else if (typeof base === 'string') { if (!_dtButtons[base]) { return { html: base }; } base = _dtButtons[base]; } loop++; if (loop > 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, e) { 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 hostNode = 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'); }; 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(); } // 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); hostNode.attr('aria-expanded', 'true'); if (hostNode.parents('body')[0] !== document.body) { hostNode = document.body.lastChild; } 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; } if (left + popoverSizes.width > tableSizes.width) { left = tableSizes.width - popoverSizes.width; } } // 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 (containerPosition.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 position = function () { var half = $(window).height() / 2; var top = display.height() / 2; if (top > half) { top = half; } display.css('marginTop', top * -1); }; position(); $(window).on('resize.dtb-collection', function () { position(); }); } 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') { // 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 (typeof str !== 'string') { return str; } // Always remove script tags str = str.replace(/)<[^<]*)*<\/script>/gi, ''); // Always remove comments str = str.replace(//g, ''); if (!config || config.stripHtml) { str = str.replace(/<[^>]*>/g, ''); } if (!config || config.trim) { str = str.replace(/^\s+|\s+$/g, ''); } if (!config || config.stripNewlines) { str = str.replace(/\n/g, ' '); } if (!config || config.decodeEntities) { _exportTextarea.innerHTML = str; str = _exportTextarea.value; } return str; }; /** * Buttons defaults. For full documentation, please refer to the docs/option * directory or the DataTables site. * @type {Object} * @static */ Buttons.defaults = { buttons: ['copy', 'excel', 'csv', 'pdf', 'print'], name: 'main', tabIndex: 0, dom: { container: { tag: 'div', className: 'dt-buttons' }, collection: { action: { // action button dropHtml: '' }, container: { // The element used for the dropdown className: 'dt-button-collection', content: { className: '', tag: 'div' }, tag: 'div' } // optionally // , button: IButton - buttons inside the collection container // , split: ISplit - splits inside the collection container }, button: { tag: 'button', className: 'dt-button', active: 'dt-button-active', // class name disabled: 'disabled', // class name spacer: { className: 'dt-button-spacer', tag: 'span' }, liner: { tag: 'span', className: '' } }, split: { action: { // action button className: 'dt-button-split-drop-button dt-button', tag: 'button' }, dropdown: { // button to trigger the dropdown align: 'split-right', className: 'dt-button-split-drop', dropHtml: '', splitAlignClass: 'dt-button-split-left', tag: 'button' }, wrapper: { // wrap around both className: 'dt-button-split', tag: 'div' } } } }; /** * Version information * @type {string} * @static */ Buttons.version = '2.4.1'; $.extend(_dtButtons, { collection: { text: function (dt) { return dt.i18n('buttons.collection', 'Collection'); }, className: 'buttons-collection', closeButton: false, init: function (dt, button, config) { button.attr('aria-expanded', false); }, action: function (e, dt, button, config) { if (config._collection.parents('body').length) { this.popover(false, config); } else { this.popover(config._collection, config); } // When activated using a key - auto focus on the // first item in the popover if (e.type === 'keypress') { $('a, button', config._collection).eq(0).focus(); } }, attr: { 'aria-haspopup': 'dialog' } // Also the popover options, defined in Buttons.popover }, split: { text: function (dt) { return dt.i18n('buttons.split', 'Split'); }, className: 'buttons-split', closeButton: false, init: function (dt, button, config) { return button.attr('aria-expanded', false); }, action: function (e, dt, button, config) { this.popover(config._collection, config); }, attr: { 'aria-haspopup': 'dialog' } // Also the popover options, defined in Buttons.popover }, copy: function (dt, conf) { if (_dtButtons.copyHtml5) { return 'copyHtml5'; } }, csv: function (dt, conf) { if (_dtButtons.csvHtml5 && _dtButtons.csvHtml5.available(dt, conf)) { return 'csvHtml5'; } }, excel: function (dt, conf) { if (_dtButtons.excelHtml5 && _dtButtons.excelHtml5.available(dt, conf)) { return 'excelHtml5'; } }, pdf: function (dt, conf) { if (_dtButtons.pdfHtml5 && _dtButtons.pdfHtml5.available(dt, conf)) { return 'pdfHtml5'; } }, pageLength: function (dt) { var lengthMenu = dt.settings()[0].aLengthMenu; var vals = []; var lang = []; var text = function (dt) { return dt.i18n( 'buttons.pageLength', { '-1': 'Show all rows', _: 'Show %d rows' }, dt.page.len() ); }; // Support for DataTables 1.x 2D array if (Array.isArray(lengthMenu[0])) { vals = lengthMenu[0]; lang = lengthMenu[1]; } else { for (var i = 0; i < lengthMenu.length; i++) { var option = lengthMenu[i]; // Support for DataTables 2 object in the array if ($.isPlainObject(option)) { vals.push(option.value); lang.push(option.label); } else { vals.push(option); lang.push(option); } } } return { extend: 'collection', text: text, className: 'buttons-page-length', autoClose: true, buttons: $.map(vals, function (val, i) { return { text: lang[i], className: 'button-page-length', action: function (e, dt) { dt.page.len(val).draw(); }, init: function (dt, node, conf) { var that = this; var fn = function () { that.active(dt.page.len() === val); }; dt.on('length.dt' + conf.namespace, fn); fn(); }, destroy: function (dt, node, conf) { dt.off('length.dt' + conf.namespace); } }; }), init: function (dt, node, conf) { var that = this; dt.on('length.dt' + conf.namespace, function () { that.text(conf.text); }); }, destroy: function (dt, node, conf) { dt.off('length.dt' + conf.namespace); } }; }, spacer: { style: 'empty', spacer: true, text: function (dt) { return dt.i18n('buttons.spacer', ''); } } }); /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * DataTables API * * For complete documentation, please refer to the docs/api directory or the * DataTables site */ // Buttons group and individual button selector DataTable.Api.register('buttons()', function (group, selector) { // Argument shifting if (selector === undefined) { selector = group; group = undefined; } this.selector.buttonGroup = group; var res = this.iterator( true, 'table', function (ctx) { if (ctx._buttons) { return Buttons.buttonSelector( Buttons.instanceSelector(group, ctx._buttons), selector ); } }, true ); res._groupSelector = group; return res; }); // Individual button selector DataTable.Api.register('button()', function (group, selector) { // just run buttons() and truncate var buttons = this.buttons(group, selector); if (buttons.length > 1) { buttons.splice(1, buttons.length); } return buttons; }); // Active buttons DataTable.Api.registerPlural('buttons().active()', 'button().active()', function (flag) { if (flag === undefined) { return this.map(function (set) { return set.inst.active(set.node); }); } return this.each(function (set) { set.inst.active(set.node, flag); }); }); // Get / set button action DataTable.Api.registerPlural('buttons().action()', 'button().action()', function (action) { if (action === undefined) { return this.map(function (set) { return set.inst.action(set.node); }); } return this.each(function (set) { set.inst.action(set.node, action); }); }); // Collection control DataTable.Api.registerPlural( 'buttons().collectionRebuild()', 'button().collectionRebuild()', function (buttons) { return this.each(function (set) { for (var i = 0; i < buttons.length; i++) { if (typeof buttons[i] === 'object') { buttons[i].parentConf = set; } } set.inst.collectionRebuild(set.node, buttons); }); } ); // Enable / disable buttons DataTable.Api.register(['buttons().enable()', 'button().enable()'], function (flag) { return this.each(function (set) { set.inst.enable(set.node, flag); }); }); // Disable buttons DataTable.Api.register(['buttons().disable()', 'button().disable()'], function () { return this.each(function (set) { set.inst.disable(set.node); }); }); // Button index DataTable.Api.register('button().index()', function () { var idx = null; this.each(function (set) { var res = set.inst.index(set.node); if (res !== null) { idx = res; } }); return idx; }); // Get button nodes DataTable.Api.registerPlural('buttons().nodes()', 'button().node()', function () { var jq = $(); // jQuery will automatically reduce duplicates to a single entry $( this.each(function (set) { jq = jq.add(set.inst.node(set.node)); }) ); return jq; }); // Get / set button processing state DataTable.Api.registerPlural('buttons().processing()', 'button().processing()', function (flag) { if (flag === undefined) { return this.map(function (set) { return set.inst.processing(set.node); }); } return this.each(function (set) { set.inst.processing(set.node, flag); }); }); // Get / set button text (i.e. the button labels) DataTable.Api.registerPlural('buttons().text()', 'button().text()', function (label) { if (label === undefined) { return this.map(function (set) { return set.inst.text(set.node); }); } return this.each(function (set) { set.inst.text(set.node, label); }); }); // Trigger a button's action DataTable.Api.registerPlural('buttons().trigger()', 'button().trigger()', function () { return this.each(function (set) { set.inst.node(set.node).trigger('click'); }); }); // Button resolver to the popover DataTable.Api.register('button().popover()', function (content, options) { return this.map(function (set) { return set.inst._popover(content, this.button(this[0].node), options); }); }); // Get the container elements DataTable.Api.register('buttons().containers()', function () { var jq = $(); var groupSelector = this._groupSelector; // We need to use the group selector directly, since if there are no buttons // the result set will be empty this.iterator(true, 'table', function (ctx) { if (ctx._buttons) { var insts = Buttons.instanceSelector(groupSelector, ctx._buttons); for (var i = 0, ien = insts.length; i < ien; i++) { jq = jq.add(insts[i].container()); } } }); return jq; }); DataTable.Api.register('buttons().container()', function () { // API level of nesting is `buttons()` so we can zip into the containers method return this.containers().eq(0); }); // Add a new button DataTable.Api.register('button().add()', function (idx, conf, draw) { var ctx = this.context; // Don't use `this` as it could be empty - select the instances directly if (ctx.length) { var inst = Buttons.instanceSelector(this._groupSelector, ctx[0]._buttons); if (inst.length) { inst[0].add(conf, idx, draw); } } return this.button(this._groupSelector, idx); }); // Destroy the button sets selected DataTable.Api.register('buttons().destroy()', function () { this.pluck('inst') .unique() .each(function (inst) { inst.destroy(); }); return this; }); // Remove a button DataTable.Api.registerPlural('buttons().remove()', 'buttons().remove()', function () { this.each(function (set) { set.inst.remove(set.node); }); return this; }); // Information box that can be used by buttons var _infoTimer; DataTable.Api.register('buttons.info()', function (title, message, time) { var that = this; if (title === false) { this.off('destroy.btn-info'); _fadeOut($('#datatables_buttons_info'), 400, function () { $(this).remove(); }); clearTimeout(_infoTimer); _infoTimer = null; return this; } if (_infoTimer) { clearTimeout(_infoTimer); } if ($('#datatables_buttons_info').length) { $('#datatables_buttons_info').remove(); } title = title ? '

' + title + '

' : ''; _fadeIn( $('
') .html(title) .append($('
')[typeof message === 'string' ? 'html' : 'append'](message)) .css('display', 'none') .appendTo('body') ); if (time !== undefined && time !== 0) { _infoTimer = setTimeout(function () { that.buttons.info(false); }, time); } this.on('destroy.btn-info', function () { that.buttons.info(false); }); return this; }); // Get data from the table for export - this is common to a number of plug-in // buttons so it is included in the Buttons core library DataTable.Api.register('buttons.exportData()', function (options) { if (this.context.length) { return _exportData(new DataTable.Api(this.context[0]), options); } }); // Get information about the export that is common to many of the export data // types (DRY) DataTable.Api.register('buttons.exportInfo()', function (conf) { if (!conf) { conf = {}; } return { filename: _filename(conf), title: _title(conf), messageTop: _message(this, conf.message || conf.messageTop, 'top'), messageBottom: _message(this, conf.messageBottom, 'bottom') }; }); /** * Get the file name for an exported file. * * @param {object} config Button configuration * @param {boolean} incExtension Include the file name extension */ var _filename = function (config) { // Backwards compatibility var filename = config.filename === '*' && config.title !== '*' && config.title !== undefined && config.title !== null && config.title !== '' ? config.title : config.filename; if (typeof filename === 'function') { filename = filename(); } if (filename === undefined || filename === null) { return null; } if (filename.indexOf('*') !== -1) { filename = filename.replace('*', $('head > title').text()).trim(); } // Strip characters which the OS will object to filename = filename.replace(/[^a-zA-Z0-9_\u00A1-\uFFFF\.,\-_ !\(\)]/g, ''); var extension = _stringOrFunction(config.extension); if (!extension) { extension = ''; } return filename + extension; }; /** * Simply utility method to allow parameters to be given as a function * * @param {undefined|string|function} option Option * @return {null|string} Resolved value */ var _stringOrFunction = function (option) { if (option === null || option === undefined) { return null; } else if (typeof option === 'function') { return option(); } return option; }; /** * Get the title for an exported file. * * @param {object} config Button configuration */ var _title = function (config) { var title = _stringOrFunction(config.title); return title === null ? null : title.indexOf('*') !== -1 ? title.replace('*', $('head > title').text() || 'Exported data') : title; }; var _message = function (dt, option, position) { var message = _stringOrFunction(option); if (message === null) { return null; } var caption = $('caption', dt.table().container()).eq(0); if (message === '*') { var side = caption.css('caption-side'); if (side !== position) { return null; } return caption.length ? caption.text() : ''; } return message; }; var _exportTextarea = $('