1/*! Buttons for DataTables 2.4.1 2 * © SpryMedia Ltd - datatables.net/license 3 */ 4 5(function( factory ){ 6 if ( typeof define === 'function' && define.amd ) { 7 // AMD 8 define( ['jquery', 'datatables.net'], function ( $ ) { 9 return factory( $, window, document ); 10 } ); 11 } 12 else if ( typeof exports === 'object' ) { 13 // CommonJS 14 var jq = require('jquery'); 15 var cjsRequires = function (root, $) { 16 if ( ! $.fn.dataTable ) { 17 require('datatables.net')(root, $); 18 } 19 }; 20 21 if (typeof window === 'undefined') { 22 module.exports = function (root, $) { 23 if ( ! root ) { 24 // CommonJS environments without a window global must pass a 25 // root. This will give an error otherwise 26 root = window; 27 } 28 29 if ( ! $ ) { 30 $ = jq( root ); 31 } 32 33 cjsRequires( root, $ ); 34 return factory( $, root, root.document ); 35 }; 36 } 37 else { 38 cjsRequires( window, jq ); 39 module.exports = factory( jq, window, window.document ); 40 } 41 } 42 else { 43 // Browser 44 factory( jQuery, window, document ); 45 } 46}(function( $, window, document, undefined ) { 47'use strict'; 48var DataTable = $.fn.dataTable; 49 50 51 52// Used for namespacing events added to the document by each instance, so they 53// can be removed on destroy 54var _instCounter = 0; 55 56// Button namespacing counter for namespacing events on individual buttons 57var _buttonCounter = 0; 58 59var _dtButtons = DataTable.ext.buttons; 60 61// Allow for jQuery slim 62function _fadeIn(el, duration, fn) { 63 if ($.fn.animate) { 64 el.stop().fadeIn(duration, fn); 65 } 66 else { 67 el.css('display', 'block'); 68 69 if (fn) { 70 fn.call(el); 71 } 72 } 73} 74 75function _fadeOut(el, duration, fn) { 76 if ($.fn.animate) { 77 el.stop().fadeOut(duration, fn); 78 } 79 else { 80 el.css('display', 'none'); 81 82 if (fn) { 83 fn.call(el); 84 } 85 } 86} 87 88/** 89 * [Buttons description] 90 * @param {[type]} 91 * @param {[type]} 92 */ 93var Buttons = function (dt, config) { 94 // If not created with a `new` keyword then we return a wrapper function that 95 // will take the settings object for a DT. This allows easy use of new instances 96 // with the `layout` option - e.g. `topLeft: $.fn.dataTable.Buttons( ... )`. 97 if (!(this instanceof Buttons)) { 98 return function (settings) { 99 return new Buttons(settings, dt).container(); 100 }; 101 } 102 103 // If there is no config set it to an empty object 104 if (typeof config === 'undefined') { 105 config = {}; 106 } 107 108 // Allow a boolean true for defaults 109 if (config === true) { 110 config = {}; 111 } 112 113 // For easy configuration of buttons an array can be given 114 if (Array.isArray(config)) { 115 config = { buttons: config }; 116 } 117 118 this.c = $.extend(true, {}, Buttons.defaults, config); 119 120 // Don't want a deep copy for the buttons 121 if (config.buttons) { 122 this.c.buttons = config.buttons; 123 } 124 125 this.s = { 126 dt: new DataTable.Api(dt), 127 buttons: [], 128 listenKeys: '', 129 namespace: 'dtb' + _instCounter++ 130 }; 131 132 this.dom = { 133 container: $('<' + this.c.dom.container.tag + '/>').addClass(this.c.dom.container.className) 134 }; 135 136 this._constructor(); 137}; 138 139$.extend(Buttons.prototype, { 140 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 141 * Public methods 142 */ 143 144 /** 145 * Get the action of a button 146 * @param {int|string} Button index 147 * @return {function} 148 */ /** 149 * Set the action of a button 150 * @param {node} node Button element 151 * @param {function} action Function to set 152 * @return {Buttons} Self for chaining 153 */ 154 action: function (node, action) { 155 var button = this._nodeToButton(node); 156 157 if (action === undefined) { 158 return button.conf.action; 159 } 160 161 button.conf.action = action; 162 163 return this; 164 }, 165 166 /** 167 * Add an active class to the button to make to look active or get current 168 * active state. 169 * @param {node} node Button element 170 * @param {boolean} [flag] Enable / disable flag 171 * @return {Buttons} Self for chaining or boolean for getter 172 */ 173 active: function (node, flag) { 174 var button = this._nodeToButton(node); 175 var klass = this.c.dom.button.active; 176 var jqNode = $(button.node); 177 178 if ( 179 button.inCollection && 180 this.c.dom.collection.button && 181 this.c.dom.collection.button.active !== undefined 182 ) { 183 klass = this.c.dom.collection.button.active; 184 } 185 186 if (flag === undefined) { 187 return jqNode.hasClass(klass); 188 } 189 190 jqNode.toggleClass(klass, flag === undefined ? true : flag); 191 192 return this; 193 }, 194 195 /** 196 * Add a new button 197 * @param {object} config Button configuration object, base string name or function 198 * @param {int|string} [idx] Button index for where to insert the button 199 * @param {boolean} [draw=true] Trigger a draw. Set a false when adding 200 * lots of buttons, until the last button. 201 * @return {Buttons} Self for chaining 202 */ 203 add: function (config, idx, draw) { 204 var buttons = this.s.buttons; 205 206 if (typeof idx === 'string') { 207 var split = idx.split('-'); 208 var base = this.s; 209 210 for (var i = 0, ien = split.length - 1; i < ien; i++) { 211 base = base.buttons[split[i] * 1]; 212 } 213 214 buttons = base.buttons; 215 idx = split[split.length - 1] * 1; 216 } 217 218 this._expandButton( 219 buttons, 220 config, 221 config !== undefined ? config.split : undefined, 222 (config === undefined || config.split === undefined || config.split.length === 0) && 223 base !== undefined, 224 false, 225 idx 226 ); 227 228 if (draw === undefined || draw === true) { 229 this._draw(); 230 } 231 232 return this; 233 }, 234 235 /** 236 * Clear buttons from a collection and then insert new buttons 237 */ 238 collectionRebuild: function (node, newButtons) { 239 var button = this._nodeToButton(node); 240 241 if (newButtons !== undefined) { 242 var i; 243 // Need to reverse the array 244 for (i = button.buttons.length - 1; i >= 0; i--) { 245 this.remove(button.buttons[i].node); 246 } 247 248 // If the collection has prefix and / or postfix buttons we need to add them in 249 if (button.conf.prefixButtons) { 250 newButtons.unshift.apply(newButtons, button.conf.prefixButtons); 251 } 252 253 if (button.conf.postfixButtons) { 254 newButtons.push.apply(newButtons, button.conf.postfixButtons); 255 } 256 257 for (i = 0; i < newButtons.length; i++) { 258 var newBtn = newButtons[i]; 259 260 this._expandButton( 261 button.buttons, 262 newBtn, 263 newBtn !== undefined && 264 newBtn.config !== undefined && 265 newBtn.config.split !== undefined, 266 true, 267 newBtn.parentConf !== undefined && newBtn.parentConf.split !== undefined, 268 null, 269 newBtn.parentConf 270 ); 271 } 272 } 273 274 this._draw(button.collection, button.buttons); 275 }, 276 277 /** 278 * Get the container node for the buttons 279 * @return {jQuery} Buttons node 280 */ 281 container: function () { 282 return this.dom.container; 283 }, 284 285 /** 286 * Disable a button 287 * @param {node} node Button node 288 * @return {Buttons} Self for chaining 289 */ 290 disable: function (node) { 291 var button = this._nodeToButton(node); 292 293 $(button.node).addClass(this.c.dom.button.disabled).prop('disabled', true); 294 295 return this; 296 }, 297 298 /** 299 * Destroy the instance, cleaning up event handlers and removing DOM 300 * elements 301 * @return {Buttons} Self for chaining 302 */ 303 destroy: function () { 304 // Key event listener 305 $('body').off('keyup.' + this.s.namespace); 306 307 // Individual button destroy (so they can remove their own events if 308 // needed). Take a copy as the array is modified by `remove` 309 var buttons = this.s.buttons.slice(); 310 var i, ien; 311 312 for (i = 0, ien = buttons.length; i < ien; i++) { 313 this.remove(buttons[i].node); 314 } 315 316 // Container 317 this.dom.container.remove(); 318 319 // Remove from the settings object collection 320 var buttonInsts = this.s.dt.settings()[0]; 321 322 for (i = 0, ien = buttonInsts.length; i < ien; i++) { 323 if (buttonInsts.inst === this) { 324 buttonInsts.splice(i, 1); 325 break; 326 } 327 } 328 329 return this; 330 }, 331 332 /** 333 * Enable / disable a button 334 * @param {node} node Button node 335 * @param {boolean} [flag=true] Enable / disable flag 336 * @return {Buttons} Self for chaining 337 */ 338 enable: function (node, flag) { 339 if (flag === false) { 340 return this.disable(node); 341 } 342 343 var button = this._nodeToButton(node); 344 $(button.node).removeClass(this.c.dom.button.disabled).prop('disabled', false); 345 346 return this; 347 }, 348 349 /** 350 * Get a button's index 351 * 352 * This is internally recursive 353 * @param {element} node Button to get the index of 354 * @return {string} Button index 355 */ 356 index: function (node, nested, buttons) { 357 if (!nested) { 358 nested = ''; 359 buttons = this.s.buttons; 360 } 361 362 for (var i = 0, ien = buttons.length; i < ien; i++) { 363 var inner = buttons[i].buttons; 364 365 if (buttons[i].node === node) { 366 return nested + i; 367 } 368 369 if (inner && inner.length) { 370 var match = this.index(node, i + '-', inner); 371 372 if (match !== null) { 373 return match; 374 } 375 } 376 } 377 378 return null; 379 }, 380 381 /** 382 * Get the instance name for the button set selector 383 * @return {string} Instance name 384 */ 385 name: function () { 386 return this.c.name; 387 }, 388 389 /** 390 * Get a button's node of the buttons container if no button is given 391 * @param {node} [node] Button node 392 * @return {jQuery} Button element, or container 393 */ 394 node: function (node) { 395 if (!node) { 396 return this.dom.container; 397 } 398 399 var button = this._nodeToButton(node); 400 return $(button.node); 401 }, 402 403 /** 404 * Set / get a processing class on the selected button 405 * @param {element} node Triggering button node 406 * @param {boolean} flag true to add, false to remove, undefined to get 407 * @return {boolean|Buttons} Getter value or this if a setter. 408 */ 409 processing: function (node, flag) { 410 var dt = this.s.dt; 411 var button = this._nodeToButton(node); 412 413 if (flag === undefined) { 414 return $(button.node).hasClass('processing'); 415 } 416 417 $(button.node).toggleClass('processing', flag); 418 419 $(dt.table().node()).triggerHandler('buttons-processing.dt', [ 420 flag, 421 dt.button(node), 422 dt, 423 $(node), 424 button.conf 425 ]); 426 427 return this; 428 }, 429 430 /** 431 * Remove a button. 432 * @param {node} node Button node 433 * @return {Buttons} Self for chaining 434 */ 435 remove: function (node) { 436 var button = this._nodeToButton(node); 437 var host = this._nodeToHost(node); 438 var dt = this.s.dt; 439 440 // Remove any child buttons first 441 if (button.buttons.length) { 442 for (var i = button.buttons.length - 1; i >= 0; i--) { 443 this.remove(button.buttons[i].node); 444 } 445 } 446 447 button.conf.destroying = true; 448 449 // Allow the button to remove event handlers, etc 450 if (button.conf.destroy) { 451 button.conf.destroy.call(dt.button(node), dt, $(node), button.conf); 452 } 453 454 this._removeKey(button.conf); 455 456 $(button.node).remove(); 457 458 var idx = $.inArray(button, host); 459 host.splice(idx, 1); 460 461 return this; 462 }, 463 464 /** 465 * Get the text for a button 466 * @param {int|string} node Button index 467 * @return {string} Button text 468 */ /** 469 * Set the text for a button 470 * @param {int|string|function} node Button index 471 * @param {string} label Text 472 * @return {Buttons} Self for chaining 473 */ 474 text: function (node, label) { 475 var button = this._nodeToButton(node); 476 var textNode = button.textNode; 477 var dt = this.s.dt; 478 var jqNode = $(button.node); 479 var text = function (opt) { 480 return typeof opt === 'function' ? opt(dt, jqNode, button.conf) : opt; 481 }; 482 483 if (label === undefined) { 484 return text(button.conf.text); 485 } 486 487 button.conf.text = label; 488 textNode.html(text(label)); 489 490 return this; 491 }, 492 493 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 494 * Constructor 495 */ 496 497 /** 498 * Buttons constructor 499 * @private 500 */ 501 _constructor: function () { 502 var that = this; 503 var dt = this.s.dt; 504 var dtSettings = dt.settings()[0]; 505 var buttons = this.c.buttons; 506 507 if (!dtSettings._buttons) { 508 dtSettings._buttons = []; 509 } 510 511 dtSettings._buttons.push({ 512 inst: this, 513 name: this.c.name 514 }); 515 516 for (var i = 0, ien = buttons.length; i < ien; i++) { 517 this.add(buttons[i]); 518 } 519 520 dt.on('destroy', function (e, settings) { 521 if (settings === dtSettings) { 522 that.destroy(); 523 } 524 }); 525 526 // Global key event binding to listen for button keys 527 $('body').on('keyup.' + this.s.namespace, function (e) { 528 if (!document.activeElement || document.activeElement === document.body) { 529 // SUse a string of characters for fast lookup of if we need to 530 // handle this 531 var character = String.fromCharCode(e.keyCode).toLowerCase(); 532 533 if (that.s.listenKeys.toLowerCase().indexOf(character) !== -1) { 534 that._keypress(character, e); 535 } 536 } 537 }); 538 }, 539 540 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 541 * Private methods 542 */ 543 544 /** 545 * Add a new button to the key press listener 546 * @param {object} conf Resolved button configuration object 547 * @private 548 */ 549 _addKey: function (conf) { 550 if (conf.key) { 551 this.s.listenKeys += $.isPlainObject(conf.key) ? conf.key.key : conf.key; 552 } 553 }, 554 555 /** 556 * Insert the buttons into the container. Call without parameters! 557 * @param {node} [container] Recursive only - Insert point 558 * @param {array} [buttons] Recursive only - Buttons array 559 * @private 560 */ 561 _draw: function (container, buttons) { 562 if (!container) { 563 container = this.dom.container; 564 buttons = this.s.buttons; 565 } 566 567 container.children().detach(); 568 569 for (var i = 0, ien = buttons.length; i < ien; i++) { 570 container.append(buttons[i].inserter); 571 container.append(' '); 572 573 if (buttons[i].buttons && buttons[i].buttons.length) { 574 this._draw(buttons[i].collection, buttons[i].buttons); 575 } 576 } 577 }, 578 579 /** 580 * Create buttons from an array of buttons 581 * @param {array} attachTo Buttons array to attach to 582 * @param {object} button Button definition 583 * @param {boolean} inCollection true if the button is in a collection 584 * @private 585 */ 586 _expandButton: function ( 587 attachTo, 588 button, 589 split, 590 inCollection, 591 inSplit, 592 attachPoint, 593 parentConf 594 ) { 595 var dt = this.s.dt; 596 var isSplit = false; 597 var domCollection = this.c.dom.collection; 598 var buttons = !Array.isArray(button) ? [button] : button; 599 600 if (button === undefined) { 601 buttons = !Array.isArray(split) ? [split] : split; 602 } 603 604 for (var i = 0, ien = buttons.length; i < ien; i++) { 605 var conf = this._resolveExtends(buttons[i]); 606 607 if (!conf) { 608 continue; 609 } 610 611 isSplit = conf.config && conf.config.split ? true : false; 612 613 // If the configuration is an array, then expand the buttons at this 614 // point 615 if (Array.isArray(conf)) { 616 this._expandButton( 617 attachTo, 618 conf, 619 built !== undefined && built.conf !== undefined ? built.conf.split : undefined, 620 inCollection, 621 parentConf !== undefined && parentConf.split !== undefined, 622 attachPoint, 623 parentConf 624 ); 625 continue; 626 } 627 628 var built = this._buildButton( 629 conf, 630 inCollection, 631 conf.split !== undefined || 632 (conf.config !== undefined && conf.config.split !== undefined), 633 inSplit 634 ); 635 if (!built) { 636 continue; 637 } 638 639 if (attachPoint !== undefined && attachPoint !== null) { 640 attachTo.splice(attachPoint, 0, built); 641 attachPoint++; 642 } 643 else { 644 attachTo.push(built); 645 } 646 647 // Create the dropdown for a collection 648 if (built.conf.buttons) { 649 built.collection = $('<' + domCollection.container.content.tag + '/>'); 650 built.conf._collection = built.collection; 651 652 $(built.node).append(domCollection.action.dropHtml); 653 654 this._expandButton( 655 built.buttons, 656 built.conf.buttons, 657 built.conf.split, 658 !isSplit, 659 isSplit, 660 attachPoint, 661 built.conf 662 ); 663 } 664 665 // And the split collection 666 if (built.conf.split) { 667 built.collection = $('<' + domCollection.container.tag + '/>'); 668 built.conf._collection = built.collection; 669 670 for (var j = 0; j < built.conf.split.length; j++) { 671 var item = built.conf.split[j]; 672 673 if (typeof item === 'object') { 674 item.parent = parentConf; 675 676 if (item.collectionLayout === undefined) { 677 item.collectionLayout = built.conf.collectionLayout; 678 } 679 680 if (item.dropup === undefined) { 681 item.dropup = built.conf.dropup; 682 } 683 684 if (item.fade === undefined) { 685 item.fade = built.conf.fade; 686 } 687 } 688 } 689 690 this._expandButton( 691 built.buttons, 692 built.conf.buttons, 693 built.conf.split, 694 !isSplit, 695 isSplit, 696 attachPoint, 697 built.conf 698 ); 699 } 700 701 built.conf.parent = parentConf; 702 703 // init call is made here, rather than buildButton as it needs to 704 // be selectable, and for that it needs to be in the buttons array 705 if (conf.init) { 706 conf.init.call(dt.button(built.node), dt, $(built.node), conf); 707 } 708 } 709 }, 710 711 /** 712 * Create an individual button 713 * @param {object} config Resolved button configuration 714 * @param {boolean} inCollection `true` if a collection button 715 * @return {object} Completed button description object 716 * @private 717 */ 718 _buildButton: function (config, inCollection, isSplit, inSplit) { 719 var configDom = this.c.dom; 720 var textNode; 721 var dt = this.s.dt; 722 var text = function (opt) { 723 return typeof opt === 'function' ? opt(dt, button, config) : opt; 724 }; 725 726 // Create an object that describes the button which can be in `dom.button`, or 727 // `dom.collection.button` or `dom.split.button` or `dom.collection.split.button`! 728 // Each should extend from `dom.button`. 729 var dom = $.extend(true, {}, configDom.button); 730 731 if (inCollection && isSplit && configDom.collection.split) { 732 $.extend(true, dom, configDom.collection.split.action); 733 } 734 else if (inSplit || inCollection) { 735 $.extend(true, dom, configDom.collection.button); 736 } 737 else if (isSplit) { 738 $.extend(true, dom, configDom.split.button); 739 } 740 741 // Spacers don't do much other than insert an element into the DOM 742 if (config.spacer) { 743 var spacer = $('<' + dom.spacer.tag + '/>') 744 .addClass('dt-button-spacer ' + config.style + ' ' + dom.spacer.className) 745 .html(text(config.text)); 746 747 return { 748 conf: config, 749 node: spacer, 750 inserter: spacer, 751 buttons: [], 752 inCollection: inCollection, 753 isSplit: isSplit, 754 collection: null, 755 textNode: spacer 756 }; 757 } 758 759 // Make sure that the button is available based on whatever requirements 760 // it has. For example, PDF button require pdfmake 761 if (config.available && !config.available(dt, config) && !config.hasOwnProperty('html')) { 762 return false; 763 } 764 765 var button; 766 767 if (!config.hasOwnProperty('html')) { 768 var action = function (e, dt, button, config) { 769 config.action.call(dt.button(button), e, dt, button, config); 770 771 $(dt.table().node()).triggerHandler('buttons-action.dt', [ 772 dt.button(button), 773 dt, 774 button, 775 config 776 ]); 777 }; 778 779 var tag = config.tag || dom.tag; 780 var clickBlurs = config.clickBlurs === undefined ? true : config.clickBlurs; 781 782 button = $('<' + tag + '/>') 783 .addClass(dom.className) 784 .attr('tabindex', this.s.dt.settings()[0].iTabIndex) 785 .attr('aria-controls', this.s.dt.table().node().id) 786 .on('click.dtb', function (e) { 787 e.preventDefault(); 788 789 if (!button.hasClass(dom.disabled) && config.action) { 790 action(e, dt, button, config); 791 } 792 793 if (clickBlurs) { 794 button.trigger('blur'); 795 } 796 }) 797 .on('keypress.dtb', function (e) { 798 if (e.keyCode === 13) { 799 e.preventDefault(); 800 801 if (!button.hasClass(dom.disabled) && config.action) { 802 action(e, dt, button, config); 803 } 804 } 805 }); 806 807 // Make `a` tags act like a link 808 if (tag.toLowerCase() === 'a') { 809 button.attr('href', '#'); 810 } 811 812 // Button tags should have `type=button` so they don't have any default behaviour 813 if (tag.toLowerCase() === 'button') { 814 button.attr('type', 'button'); 815 } 816 817 if (dom.liner.tag) { 818 var liner = $('<' + dom.liner.tag + '/>') 819 .html(text(config.text)) 820 .addClass(dom.liner.className); 821 822 if (dom.liner.tag.toLowerCase() === 'a') { 823 liner.attr('href', '#'); 824 } 825 826 button.append(liner); 827 textNode = liner; 828 } 829 else { 830 button.html(text(config.text)); 831 textNode = button; 832 } 833 834 if (config.enabled === false) { 835 button.addClass(dom.disabled); 836 } 837 838 if (config.className) { 839 button.addClass(config.className); 840 } 841 842 if (config.titleAttr) { 843 button.attr('title', text(config.titleAttr)); 844 } 845 846 if (config.attr) { 847 button.attr(config.attr); 848 } 849 850 if (!config.namespace) { 851 config.namespace = '.dt-button-' + _buttonCounter++; 852 } 853 854 if (config.config !== undefined && config.config.split) { 855 config.split = config.config.split; 856 } 857 } 858 else { 859 button = $(config.html); 860 } 861 862 var buttonContainer = this.c.dom.buttonContainer; 863 var inserter; 864 if (buttonContainer && buttonContainer.tag) { 865 inserter = $('<' + buttonContainer.tag + '/>') 866 .addClass(buttonContainer.className) 867 .append(button); 868 } 869 else { 870 inserter = button; 871 } 872 873 this._addKey(config); 874 875 // Style integration callback for DOM manipulation 876 // Note that this is _not_ documented. It is currently 877 // for style integration only 878 if (this.c.buttonCreated) { 879 inserter = this.c.buttonCreated(config, inserter); 880 } 881 882 var splitDiv; 883 884 if (isSplit) { 885 var dropdownConf = inCollection 886 ? $.extend(true, this.c.dom.split, this.c.dom.collection.split) 887 : this.c.dom.split; 888 var wrapperConf = dropdownConf.wrapper; 889 890 splitDiv = $('<' + wrapperConf.tag + '/>') 891 .addClass(wrapperConf.className) 892 .append(button); 893 894 var dropButtonConfig = $.extend(config, { 895 align: dropdownConf.dropdown.align, 896 attr: { 897 'aria-haspopup': 'dialog', 898 'aria-expanded': false 899 }, 900 className: dropdownConf.dropdown.className, 901 closeButton: false, 902 splitAlignClass: dropdownConf.dropdown.splitAlignClass, 903 text: dropdownConf.dropdown.text 904 }); 905 906 this._addKey(dropButtonConfig); 907 908 var splitAction = function (e, dt, button, config) { 909 _dtButtons.split.action.call(dt.button(splitDiv), e, dt, button, config); 910 911 $(dt.table().node()).triggerHandler('buttons-action.dt', [ 912 dt.button(button), 913 dt, 914 button, 915 config 916 ]); 917 button.attr('aria-expanded', true); 918 }; 919 920 var dropButton = $( 921 '<button class="' + dropdownConf.dropdown.className + ' dt-button"></button>' 922 ) 923 .html(dropdownConf.dropdown.dropHtml) 924 .on('click.dtb', function (e) { 925 e.preventDefault(); 926 e.stopPropagation(); 927 928 if (!dropButton.hasClass(dom.disabled)) { 929 splitAction(e, dt, dropButton, dropButtonConfig); 930 } 931 if (clickBlurs) { 932 dropButton.trigger('blur'); 933 } 934 }) 935 .on('keypress.dtb', function (e) { 936 if (e.keyCode === 13) { 937 e.preventDefault(); 938 939 if (!dropButton.hasClass(dom.disabled)) { 940 splitAction(e, dt, dropButton, dropButtonConfig); 941 } 942 } 943 }); 944 945 if (config.split.length === 0) { 946 dropButton.addClass('dtb-hide-drop'); 947 } 948 949 splitDiv.append(dropButton).attr(dropButtonConfig.attr); 950 } 951 952 return { 953 conf: config, 954 node: isSplit ? splitDiv.get(0) : button.get(0), 955 inserter: isSplit ? splitDiv : inserter, 956 buttons: [], 957 inCollection: inCollection, 958 isSplit: isSplit, 959 inSplit: inSplit, 960 collection: null, 961 textNode: textNode 962 }; 963 }, 964 965 /** 966 * Get the button object from a node (recursive) 967 * @param {node} node Button node 968 * @param {array} [buttons] Button array, uses base if not defined 969 * @return {object} Button object 970 * @private 971 */ 972 _nodeToButton: function (node, buttons) { 973 if (!buttons) { 974 buttons = this.s.buttons; 975 } 976 977 for (var i = 0, ien = buttons.length; i < ien; i++) { 978 if (buttons[i].node === node) { 979 return buttons[i]; 980 } 981 982 if (buttons[i].buttons.length) { 983 var ret = this._nodeToButton(node, buttons[i].buttons); 984 985 if (ret) { 986 return ret; 987 } 988 } 989 } 990 }, 991 992 /** 993 * Get container array for a button from a button node (recursive) 994 * @param {node} node Button node 995 * @param {array} [buttons] Button array, uses base if not defined 996 * @return {array} Button's host array 997 * @private 998 */ 999 _nodeToHost: function (node, buttons) { 1000 if (!buttons) { 1001 buttons = this.s.buttons; 1002 } 1003 1004 for (var i = 0, ien = buttons.length; i < ien; i++) { 1005 if (buttons[i].node === node) { 1006 return buttons; 1007 } 1008 1009 if (buttons[i].buttons.length) { 1010 var ret = this._nodeToHost(node, buttons[i].buttons); 1011 1012 if (ret) { 1013 return ret; 1014 } 1015 } 1016 } 1017 }, 1018 1019 /** 1020 * Handle a key press - determine if any button's key configured matches 1021 * what was typed and trigger the action if so. 1022 * @param {string} character The character pressed 1023 * @param {object} e Key event that triggered this call 1024 * @private 1025 */ 1026 _keypress: function (character, e) { 1027 // Check if this button press already activated on another instance of Buttons 1028 if (e._buttonsHandled) { 1029 return; 1030 } 1031 1032 var run = function (conf, node) { 1033 if (!conf.key) { 1034 return; 1035 } 1036 1037 if (conf.key === character) { 1038 e._buttonsHandled = true; 1039 $(node).click(); 1040 } 1041 else if ($.isPlainObject(conf.key)) { 1042 if (conf.key.key !== character) { 1043 return; 1044 } 1045 1046 if (conf.key.shiftKey && !e.shiftKey) { 1047 return; 1048 } 1049 1050 if (conf.key.altKey && !e.altKey) { 1051 return; 1052 } 1053 1054 if (conf.key.ctrlKey && !e.ctrlKey) { 1055 return; 1056 } 1057 1058 if (conf.key.metaKey && !e.metaKey) { 1059 return; 1060 } 1061 1062 // Made it this far - it is good 1063 e._buttonsHandled = true; 1064 $(node).click(); 1065 } 1066 }; 1067 1068 var recurse = function (a) { 1069 for (var i = 0, ien = a.length; i < ien; i++) { 1070 run(a[i].conf, a[i].node); 1071 1072 if (a[i].buttons.length) { 1073 recurse(a[i].buttons); 1074 } 1075 } 1076 }; 1077 1078 recurse(this.s.buttons); 1079 }, 1080 1081 /** 1082 * Remove a key from the key listener for this instance (to be used when a 1083 * button is removed) 1084 * @param {object} conf Button configuration 1085 * @private 1086 */ 1087 _removeKey: function (conf) { 1088 if (conf.key) { 1089 var character = $.isPlainObject(conf.key) ? conf.key.key : conf.key; 1090 1091 // Remove only one character, as multiple buttons could have the 1092 // same listening key 1093 var a = this.s.listenKeys.split(''); 1094 var idx = $.inArray(character, a); 1095 a.splice(idx, 1); 1096 this.s.listenKeys = a.join(''); 1097 } 1098 }, 1099 1100 /** 1101 * Resolve a button configuration 1102 * @param {string|function|object} conf Button config to resolve 1103 * @return {object} Button configuration 1104 * @private 1105 */ 1106 _resolveExtends: function (conf) { 1107 var that = this; 1108 var dt = this.s.dt; 1109 var i, ien; 1110 var toConfObject = function (base) { 1111 var loop = 0; 1112 1113 // Loop until we have resolved to a button configuration, or an 1114 // array of button configurations (which will be iterated 1115 // separately) 1116 while (!$.isPlainObject(base) && !Array.isArray(base)) { 1117 if (base === undefined) { 1118 return; 1119 } 1120 1121 if (typeof base === 'function') { 1122 base = base.call(that, dt, conf); 1123 1124 if (!base) { 1125 return false; 1126 } 1127 } 1128 else if (typeof base === 'string') { 1129 if (!_dtButtons[base]) { 1130 return { html: base }; 1131 } 1132 1133 base = _dtButtons[base]; 1134 } 1135 1136 loop++; 1137 if (loop > 30) { 1138 // Protect against misconfiguration killing the browser 1139 throw 'Buttons: Too many iterations'; 1140 } 1141 } 1142 1143 return Array.isArray(base) ? base : $.extend({}, base); 1144 }; 1145 1146 conf = toConfObject(conf); 1147 1148 while (conf && conf.extend) { 1149 // Use `toConfObject` in case the button definition being extended 1150 // is itself a string or a function 1151 if (!_dtButtons[conf.extend]) { 1152 throw 'Cannot extend unknown button type: ' + conf.extend; 1153 } 1154 1155 var objArray = toConfObject(_dtButtons[conf.extend]); 1156 if (Array.isArray(objArray)) { 1157 return objArray; 1158 } 1159 else if (!objArray) { 1160 // This is a little brutal as it might be possible to have a 1161 // valid button without the extend, but if there is no extend 1162 // then the host button would be acting in an undefined state 1163 return false; 1164 } 1165 1166 // Stash the current class name 1167 var originalClassName = objArray.className; 1168 1169 if (conf.config !== undefined && objArray.config !== undefined) { 1170 conf.config = $.extend({}, objArray.config, conf.config); 1171 } 1172 1173 conf = $.extend({}, objArray, conf); 1174 1175 // The extend will have overwritten the original class name if the 1176 // `conf` object also assigned a class, but we want to concatenate 1177 // them so they are list that is combined from all extended buttons 1178 if (originalClassName && conf.className !== originalClassName) { 1179 conf.className = originalClassName + ' ' + conf.className; 1180 } 1181 1182 // Although we want the `conf` object to overwrite almost all of 1183 // the properties of the object being extended, the `extend` 1184 // property should come from the object being extended 1185 conf.extend = objArray.extend; 1186 } 1187 1188 // Buttons to be added to a collection -gives the ability to define 1189 // if buttons should be added to the start or end of a collection 1190 var postfixButtons = conf.postfixButtons; 1191 if (postfixButtons) { 1192 if (!conf.buttons) { 1193 conf.buttons = []; 1194 } 1195 1196 for (i = 0, ien = postfixButtons.length; i < ien; i++) { 1197 conf.buttons.push(postfixButtons[i]); 1198 } 1199 } 1200 1201 var prefixButtons = conf.prefixButtons; 1202 if (prefixButtons) { 1203 if (!conf.buttons) { 1204 conf.buttons = []; 1205 } 1206 1207 for (i = 0, ien = prefixButtons.length; i < ien; i++) { 1208 conf.buttons.splice(i, 0, prefixButtons[i]); 1209 } 1210 } 1211 1212 return conf; 1213 }, 1214 1215 /** 1216 * Display (and replace if there is an existing one) a popover attached to a button 1217 * @param {string|node} content Content to show 1218 * @param {DataTable.Api} hostButton DT API instance of the button 1219 * @param {object} inOpts Options (see object below for all options) 1220 */ 1221 _popover: function (content, hostButton, inOpts, e) { 1222 var dt = hostButton; 1223 var c = this.c; 1224 var closed = false; 1225 var options = $.extend( 1226 { 1227 align: 'button-left', // button-right, dt-container, split-left, split-right 1228 autoClose: false, 1229 background: true, 1230 backgroundClassName: 'dt-button-background', 1231 closeButton: true, 1232 containerClassName: c.dom.collection.container.className, 1233 contentClassName: c.dom.collection.container.content.className, 1234 collectionLayout: '', 1235 collectionTitle: '', 1236 dropup: false, 1237 fade: 400, 1238 popoverTitle: '', 1239 rightAlignClassName: 'dt-button-right', 1240 tag: c.dom.collection.container.tag 1241 }, 1242 inOpts 1243 ); 1244 1245 var containerSelector = options.tag + '.' + options.containerClassName.replace(/ /g, '.'); 1246 var hostNode = hostButton.node(); 1247 1248 var close = function () { 1249 closed = true; 1250 1251 _fadeOut($(containerSelector), options.fade, function () { 1252 $(this).detach(); 1253 }); 1254 1255 $(dt.buttons('[aria-haspopup="dialog"][aria-expanded="true"]').nodes()).attr( 1256 'aria-expanded', 1257 'false' 1258 ); 1259 1260 $('div.dt-button-background').off('click.dtb-collection'); 1261 Buttons.background(false, options.backgroundClassName, options.fade, hostNode); 1262 1263 $(window).off('resize.resize.dtb-collection'); 1264 $('body').off('.dtb-collection'); 1265 dt.off('buttons-action.b-internal'); 1266 dt.off('destroy'); 1267 }; 1268 1269 if (content === false) { 1270 close(); 1271 return; 1272 } 1273 1274 var existingExpanded = $( 1275 dt.buttons('[aria-haspopup="dialog"][aria-expanded="true"]').nodes() 1276 ); 1277 if (existingExpanded.length) { 1278 // Reuse the current position if the button that was triggered is inside an existing collection 1279 if (hostNode.closest(containerSelector).length) { 1280 hostNode = existingExpanded.eq(0); 1281 } 1282 1283 close(); 1284 } 1285 1286 // Try to be smart about the layout 1287 var cnt = $('.dt-button', content).length; 1288 var mod = ''; 1289 1290 if (cnt === 3) { 1291 mod = 'dtb-b3'; 1292 } 1293 else if (cnt === 2) { 1294 mod = 'dtb-b2'; 1295 } 1296 else if (cnt === 1) { 1297 mod = 'dtb-b1'; 1298 } 1299 1300 var display = $('<' + options.tag + '/>') 1301 .addClass(options.containerClassName) 1302 .addClass(options.collectionLayout) 1303 .addClass(options.splitAlignClass) 1304 .addClass(mod) 1305 .css('display', 'none') 1306 .attr({ 1307 'aria-modal': true, 1308 role: 'dialog' 1309 }); 1310 1311 content = $(content) 1312 .addClass(options.contentClassName) 1313 .attr('role', 'menu') 1314 .appendTo(display); 1315 1316 hostNode.attr('aria-expanded', 'true'); 1317 1318 if (hostNode.parents('body')[0] !== document.body) { 1319 hostNode = document.body.lastChild; 1320 } 1321 1322 if (options.popoverTitle) { 1323 display.prepend( 1324 '<div class="dt-button-collection-title">' + options.popoverTitle + '</div>' 1325 ); 1326 } 1327 else if (options.collectionTitle) { 1328 display.prepend( 1329 '<div class="dt-button-collection-title">' + options.collectionTitle + '</div>' 1330 ); 1331 } 1332 1333 if (options.closeButton) { 1334 display 1335 .prepend('<div class="dtb-popover-close">×</div>') 1336 .addClass('dtb-collection-closeable'); 1337 } 1338 1339 _fadeIn(display.insertAfter(hostNode), options.fade); 1340 1341 var tableContainer = $(hostButton.table().container()); 1342 var position = display.css('position'); 1343 1344 if (options.span === 'container' || options.align === 'dt-container') { 1345 hostNode = hostNode.parent(); 1346 display.css('width', tableContainer.width()); 1347 } 1348 1349 // Align the popover relative to the DataTables container 1350 // Useful for wide popovers such as SearchPanes 1351 if (position === 'absolute') { 1352 // Align relative to the host button 1353 var offsetParent = $(hostNode[0].offsetParent); 1354 var buttonPosition = hostNode.position(); 1355 var buttonOffset = hostNode.offset(); 1356 var tableSizes = offsetParent.offset(); 1357 var containerPosition = offsetParent.position(); 1358 var computed = window.getComputedStyle(offsetParent[0]); 1359 1360 tableSizes.height = offsetParent.outerHeight(); 1361 tableSizes.width = offsetParent.width() + parseFloat(computed.paddingLeft); 1362 tableSizes.right = tableSizes.left + tableSizes.width; 1363 tableSizes.bottom = tableSizes.top + tableSizes.height; 1364 1365 // Set the initial position so we can read height / width 1366 var top = buttonPosition.top + hostNode.outerHeight(); 1367 var left = buttonPosition.left; 1368 1369 display.css({ 1370 top: top, 1371 left: left 1372 }); 1373 1374 // Get the popover position 1375 computed = window.getComputedStyle(display[0]); 1376 var popoverSizes = display.offset(); 1377 1378 popoverSizes.height = display.outerHeight(); 1379 popoverSizes.width = display.outerWidth(); 1380 popoverSizes.right = popoverSizes.left + popoverSizes.width; 1381 popoverSizes.bottom = popoverSizes.top + popoverSizes.height; 1382 popoverSizes.marginTop = parseFloat(computed.marginTop); 1383 popoverSizes.marginBottom = parseFloat(computed.marginBottom); 1384 1385 // First position per the class requirements - pop up and right align 1386 if (options.dropup) { 1387 top = 1388 buttonPosition.top - 1389 popoverSizes.height - 1390 popoverSizes.marginTop - 1391 popoverSizes.marginBottom; 1392 } 1393 1394 if (options.align === 'button-right' || display.hasClass(options.rightAlignClassName)) { 1395 left = buttonPosition.left - popoverSizes.width + hostNode.outerWidth(); 1396 } 1397 1398 // Container alignment - make sure it doesn't overflow the table container 1399 if (options.align === 'dt-container' || options.align === 'container') { 1400 if (left < buttonPosition.left) { 1401 left = -buttonPosition.left; 1402 } 1403 1404 if (left + popoverSizes.width > tableSizes.width) { 1405 left = tableSizes.width - popoverSizes.width; 1406 } 1407 } 1408 1409 // Window adjustment 1410 if (containerPosition.left + left + popoverSizes.width > $(window).width()) { 1411 // Overflowing the document to the right 1412 left = $(window).width() - popoverSizes.width - containerPosition.left; 1413 } 1414 1415 if (buttonOffset.left + left < 0) { 1416 // Off to the left of the document 1417 left = -buttonOffset.left; 1418 } 1419 1420 if ( 1421 containerPosition.top + top + popoverSizes.height > 1422 $(window).height() + $(window).scrollTop() 1423 ) { 1424 // Pop up if otherwise we'd need the user to scroll down 1425 top = 1426 buttonPosition.top - 1427 popoverSizes.height - 1428 popoverSizes.marginTop - 1429 popoverSizes.marginBottom; 1430 } 1431 1432 if (containerPosition.top + top < $(window).scrollTop()) { 1433 // Correction for when the top is beyond the top of the page 1434 top = buttonPosition.top + hostNode.outerHeight(); 1435 } 1436 1437 // Calculations all done - now set it 1438 display.css({ 1439 top: top, 1440 left: left 1441 }); 1442 } 1443 else { 1444 // Fix position - centre on screen 1445 var position = function () { 1446 var half = $(window).height() / 2; 1447 1448 var top = display.height() / 2; 1449 if (top > half) { 1450 top = half; 1451 } 1452 1453 display.css('marginTop', top * -1); 1454 }; 1455 1456 position(); 1457 1458 $(window).on('resize.dtb-collection', function () { 1459 position(); 1460 }); 1461 } 1462 1463 if (options.background) { 1464 Buttons.background( 1465 true, 1466 options.backgroundClassName, 1467 options.fade, 1468 options.backgroundHost || hostNode 1469 ); 1470 } 1471 1472 // This is bonkers, but if we don't have a click listener on the 1473 // background element, iOS Safari will ignore the body click 1474 // listener below. An empty function here is all that is 1475 // required to make it work... 1476 $('div.dt-button-background').on('click.dtb-collection', function () {}); 1477 1478 if (options.autoClose) { 1479 setTimeout(function () { 1480 dt.on('buttons-action.b-internal', function (e, btn, dt, node) { 1481 if (node[0] === hostNode[0]) { 1482 return; 1483 } 1484 close(); 1485 }); 1486 }, 0); 1487 } 1488 1489 $(display).trigger('buttons-popover.dt'); 1490 1491 dt.on('destroy', close); 1492 1493 setTimeout(function () { 1494 closed = false; 1495 $('body') 1496 .on('click.dtb-collection', function (e) { 1497 if (closed) { 1498 return; 1499 } 1500 1501 // andSelf is deprecated in jQ1.8, but we want 1.7 compat 1502 var back = $.fn.addBack ? 'addBack' : 'andSelf'; 1503 var parent = $(e.target).parent()[0]; 1504 1505 if ( 1506 (!$(e.target).parents()[back]().filter(content).length && 1507 !$(parent).hasClass('dt-buttons')) || 1508 $(e.target).hasClass('dt-button-background') 1509 ) { 1510 close(); 1511 } 1512 }) 1513 .on('keyup.dtb-collection', function (e) { 1514 if (e.keyCode === 27) { 1515 close(); 1516 } 1517 }) 1518 .on('keydown.dtb-collection', function (e) { 1519 // Focus trap for tab key 1520 var elements = $('a, button', content); 1521 var active = document.activeElement; 1522 1523 if (e.keyCode !== 9) { 1524 // tab 1525 return; 1526 } 1527 1528 if (elements.index(active) === -1) { 1529 // If current focus is not inside the popover 1530 elements.first().focus(); 1531 e.preventDefault(); 1532 } 1533 else if (e.shiftKey) { 1534 // Reverse tabbing order when shift key is pressed 1535 if (active === elements[0]) { 1536 elements.last().focus(); 1537 e.preventDefault(); 1538 } 1539 } 1540 else { 1541 if (active === elements.last()[0]) { 1542 elements.first().focus(); 1543 e.preventDefault(); 1544 } 1545 } 1546 }); 1547 }, 0); 1548 } 1549}); 1550 1551/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 1552 * Statics 1553 */ 1554 1555/** 1556 * Show / hide a background layer behind a collection 1557 * @param {boolean} Flag to indicate if the background should be shown or 1558 * hidden 1559 * @param {string} Class to assign to the background 1560 * @static 1561 */ 1562Buttons.background = function (show, className, fade, insertPoint) { 1563 if (fade === undefined) { 1564 fade = 400; 1565 } 1566 if (!insertPoint) { 1567 insertPoint = document.body; 1568 } 1569 1570 if (show) { 1571 _fadeIn( 1572 $('<div/>').addClass(className).css('display', 'none').insertAfter(insertPoint), 1573 fade 1574 ); 1575 } 1576 else { 1577 _fadeOut($('div.' + className), fade, function () { 1578 $(this).removeClass(className).remove(); 1579 }); 1580 } 1581}; 1582 1583/** 1584 * Instance selector - select Buttons instances based on an instance selector 1585 * value from the buttons assigned to a DataTable. This is only useful if 1586 * multiple instances are attached to a DataTable. 1587 * @param {string|int|array} Instance selector - see `instance-selector` 1588 * documentation on the DataTables site 1589 * @param {array} Button instance array that was attached to the DataTables 1590 * settings object 1591 * @return {array} Buttons instances 1592 * @static 1593 */ 1594Buttons.instanceSelector = function (group, buttons) { 1595 if (group === undefined || group === null) { 1596 return $.map(buttons, function (v) { 1597 return v.inst; 1598 }); 1599 } 1600 1601 var ret = []; 1602 var names = $.map(buttons, function (v) { 1603 return v.name; 1604 }); 1605 1606 // Flatten the group selector into an array of single options 1607 var process = function (input) { 1608 if (Array.isArray(input)) { 1609 for (var i = 0, ien = input.length; i < ien; i++) { 1610 process(input[i]); 1611 } 1612 return; 1613 } 1614 1615 if (typeof input === 'string') { 1616 if (input.indexOf(',') !== -1) { 1617 // String selector, list of names 1618 process(input.split(',')); 1619 } 1620 else { 1621 // String selector individual name 1622 var idx = $.inArray(input.trim(), names); 1623 1624 if (idx !== -1) { 1625 ret.push(buttons[idx].inst); 1626 } 1627 } 1628 } 1629 else if (typeof input === 'number') { 1630 // Index selector 1631 ret.push(buttons[input].inst); 1632 } 1633 else if (typeof input === 'object') { 1634 // Actual instance selector 1635 ret.push(input); 1636 } 1637 }; 1638 1639 process(group); 1640 1641 return ret; 1642}; 1643 1644/** 1645 * Button selector - select one or more buttons from a selector input so some 1646 * operation can be performed on them. 1647 * @param {array} Button instances array that the selector should operate on 1648 * @param {string|int|node|jQuery|array} Button selector - see 1649 * `button-selector` documentation on the DataTables site 1650 * @return {array} Array of objects containing `inst` and `idx` properties of 1651 * the selected buttons so you know which instance each button belongs to. 1652 * @static 1653 */ 1654Buttons.buttonSelector = function (insts, selector) { 1655 var ret = []; 1656 var nodeBuilder = function (a, buttons, baseIdx) { 1657 var button; 1658 var idx; 1659 1660 for (var i = 0, ien = buttons.length; i < ien; i++) { 1661 button = buttons[i]; 1662 1663 if (button) { 1664 idx = baseIdx !== undefined ? baseIdx + i : i + ''; 1665 1666 a.push({ 1667 node: button.node, 1668 name: button.conf.name, 1669 idx: idx 1670 }); 1671 1672 if (button.buttons) { 1673 nodeBuilder(a, button.buttons, idx + '-'); 1674 } 1675 } 1676 } 1677 }; 1678 1679 var run = function (selector, inst) { 1680 var i, ien; 1681 var buttons = []; 1682 nodeBuilder(buttons, inst.s.buttons); 1683 1684 var nodes = $.map(buttons, function (v) { 1685 return v.node; 1686 }); 1687 1688 if (Array.isArray(selector) || selector instanceof $) { 1689 for (i = 0, ien = selector.length; i < ien; i++) { 1690 run(selector[i], inst); 1691 } 1692 return; 1693 } 1694 1695 if (selector === null || selector === undefined || selector === '*') { 1696 // Select all 1697 for (i = 0, ien = buttons.length; i < ien; i++) { 1698 ret.push({ 1699 inst: inst, 1700 node: buttons[i].node 1701 }); 1702 } 1703 } 1704 else if (typeof selector === 'number') { 1705 // Main button index selector 1706 if (inst.s.buttons[selector]) { 1707 ret.push({ 1708 inst: inst, 1709 node: inst.s.buttons[selector].node 1710 }); 1711 } 1712 } 1713 else if (typeof selector === 'string') { 1714 if (selector.indexOf(',') !== -1) { 1715 // Split 1716 var a = selector.split(','); 1717 1718 for (i = 0, ien = a.length; i < ien; i++) { 1719 run(a[i].trim(), inst); 1720 } 1721 } 1722 else if (selector.match(/^\d+(\-\d+)*$/)) { 1723 // Sub-button index selector 1724 var indexes = $.map(buttons, function (v) { 1725 return v.idx; 1726 }); 1727 1728 ret.push({ 1729 inst: inst, 1730 node: buttons[$.inArray(selector, indexes)].node 1731 }); 1732 } 1733 else if (selector.indexOf(':name') !== -1) { 1734 // Button name selector 1735 var name = selector.replace(':name', ''); 1736 1737 for (i = 0, ien = buttons.length; i < ien; i++) { 1738 if (buttons[i].name === name) { 1739 ret.push({ 1740 inst: inst, 1741 node: buttons[i].node 1742 }); 1743 } 1744 } 1745 } 1746 else { 1747 // jQuery selector on the nodes 1748 $(nodes) 1749 .filter(selector) 1750 .each(function () { 1751 ret.push({ 1752 inst: inst, 1753 node: this 1754 }); 1755 }); 1756 } 1757 } 1758 else if (typeof selector === 'object' && selector.nodeName) { 1759 // Node selector 1760 var idx = $.inArray(selector, nodes); 1761 1762 if (idx !== -1) { 1763 ret.push({ 1764 inst: inst, 1765 node: nodes[idx] 1766 }); 1767 } 1768 } 1769 }; 1770 1771 for (var i = 0, ien = insts.length; i < ien; i++) { 1772 var inst = insts[i]; 1773 1774 run(selector, inst); 1775 } 1776 1777 return ret; 1778}; 1779 1780/** 1781 * Default function used for formatting output data. 1782 * @param {*} str Data to strip 1783 */ 1784Buttons.stripData = function (str, config) { 1785 if (typeof str !== 'string') { 1786 return str; 1787 } 1788 1789 // Always remove script tags 1790 str = str.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); 1791 1792 // Always remove comments 1793 str = str.replace(/<!\-\-.*?\-\->/g, ''); 1794 1795 if (!config || config.stripHtml) { 1796 str = str.replace(/<[^>]*>/g, ''); 1797 } 1798 1799 if (!config || config.trim) { 1800 str = str.replace(/^\s+|\s+$/g, ''); 1801 } 1802 1803 if (!config || config.stripNewlines) { 1804 str = str.replace(/\n/g, ' '); 1805 } 1806 1807 if (!config || config.decodeEntities) { 1808 _exportTextarea.innerHTML = str; 1809 str = _exportTextarea.value; 1810 } 1811 1812 return str; 1813}; 1814 1815/** 1816 * Buttons defaults. For full documentation, please refer to the docs/option 1817 * directory or the DataTables site. 1818 * @type {Object} 1819 * @static 1820 */ 1821Buttons.defaults = { 1822 buttons: ['copy', 'excel', 'csv', 'pdf', 'print'], 1823 name: 'main', 1824 tabIndex: 0, 1825 dom: { 1826 container: { 1827 tag: 'div', 1828 className: 'dt-buttons' 1829 }, 1830 collection: { 1831 action: { 1832 // action button 1833 dropHtml: '<span class="dt-button-down-arrow">▼</span>' 1834 }, 1835 container: { 1836 // The element used for the dropdown 1837 className: 'dt-button-collection', 1838 content: { 1839 className: '', 1840 tag: 'div' 1841 }, 1842 tag: 'div' 1843 } 1844 // optionally 1845 // , button: IButton - buttons inside the collection container 1846 // , split: ISplit - splits inside the collection container 1847 }, 1848 button: { 1849 tag: 'button', 1850 className: 'dt-button', 1851 active: 'dt-button-active', // class name 1852 disabled: 'disabled', // class name 1853 spacer: { 1854 className: 'dt-button-spacer', 1855 tag: 'span' 1856 }, 1857 liner: { 1858 tag: 'span', 1859 className: '' 1860 } 1861 }, 1862 split: { 1863 action: { 1864 // action button 1865 className: 'dt-button-split-drop-button dt-button', 1866 tag: 'button' 1867 }, 1868 dropdown: { 1869 // button to trigger the dropdown 1870 align: 'split-right', 1871 className: 'dt-button-split-drop', 1872 dropHtml: '<span class="dt-button-down-arrow">▼</span>', 1873 splitAlignClass: 'dt-button-split-left', 1874 tag: 'button' 1875 }, 1876 wrapper: { 1877 // wrap around both 1878 className: 'dt-button-split', 1879 tag: 'div' 1880 } 1881 } 1882 } 1883}; 1884 1885/** 1886 * Version information 1887 * @type {string} 1888 * @static 1889 */ 1890Buttons.version = '2.4.1'; 1891 1892$.extend(_dtButtons, { 1893 collection: { 1894 text: function (dt) { 1895 return dt.i18n('buttons.collection', 'Collection'); 1896 }, 1897 className: 'buttons-collection', 1898 closeButton: false, 1899 init: function (dt, button, config) { 1900 button.attr('aria-expanded', false); 1901 }, 1902 action: function (e, dt, button, config) { 1903 if (config._collection.parents('body').length) { 1904 this.popover(false, config); 1905 } 1906 else { 1907 this.popover(config._collection, config); 1908 } 1909 1910 // When activated using a key - auto focus on the 1911 // first item in the popover 1912 if (e.type === 'keypress') { 1913 $('a, button', config._collection).eq(0).focus(); 1914 } 1915 }, 1916 attr: { 1917 'aria-haspopup': 'dialog' 1918 } 1919 // Also the popover options, defined in Buttons.popover 1920 }, 1921 split: { 1922 text: function (dt) { 1923 return dt.i18n('buttons.split', 'Split'); 1924 }, 1925 className: 'buttons-split', 1926 closeButton: false, 1927 init: function (dt, button, config) { 1928 return button.attr('aria-expanded', false); 1929 }, 1930 action: function (e, dt, button, config) { 1931 this.popover(config._collection, config); 1932 }, 1933 attr: { 1934 'aria-haspopup': 'dialog' 1935 } 1936 // Also the popover options, defined in Buttons.popover 1937 }, 1938 copy: function (dt, conf) { 1939 if (_dtButtons.copyHtml5) { 1940 return 'copyHtml5'; 1941 } 1942 }, 1943 csv: function (dt, conf) { 1944 if (_dtButtons.csvHtml5 && _dtButtons.csvHtml5.available(dt, conf)) { 1945 return 'csvHtml5'; 1946 } 1947 }, 1948 excel: function (dt, conf) { 1949 if (_dtButtons.excelHtml5 && _dtButtons.excelHtml5.available(dt, conf)) { 1950 return 'excelHtml5'; 1951 } 1952 }, 1953 pdf: function (dt, conf) { 1954 if (_dtButtons.pdfHtml5 && _dtButtons.pdfHtml5.available(dt, conf)) { 1955 return 'pdfHtml5'; 1956 } 1957 }, 1958 pageLength: function (dt) { 1959 var lengthMenu = dt.settings()[0].aLengthMenu; 1960 var vals = []; 1961 var lang = []; 1962 var text = function (dt) { 1963 return dt.i18n( 1964 'buttons.pageLength', 1965 { 1966 '-1': 'Show all rows', 1967 _: 'Show %d rows' 1968 }, 1969 dt.page.len() 1970 ); 1971 }; 1972 1973 // Support for DataTables 1.x 2D array 1974 if (Array.isArray(lengthMenu[0])) { 1975 vals = lengthMenu[0]; 1976 lang = lengthMenu[1]; 1977 } 1978 else { 1979 for (var i = 0; i < lengthMenu.length; i++) { 1980 var option = lengthMenu[i]; 1981 1982 // Support for DataTables 2 object in the array 1983 if ($.isPlainObject(option)) { 1984 vals.push(option.value); 1985 lang.push(option.label); 1986 } 1987 else { 1988 vals.push(option); 1989 lang.push(option); 1990 } 1991 } 1992 } 1993 1994 return { 1995 extend: 'collection', 1996 text: text, 1997 className: 'buttons-page-length', 1998 autoClose: true, 1999 buttons: $.map(vals, function (val, i) { 2000 return { 2001 text: lang[i], 2002 className: 'button-page-length', 2003 action: function (e, dt) { 2004 dt.page.len(val).draw(); 2005 }, 2006 init: function (dt, node, conf) { 2007 var that = this; 2008 var fn = function () { 2009 that.active(dt.page.len() === val); 2010 }; 2011 2012 dt.on('length.dt' + conf.namespace, fn); 2013 fn(); 2014 }, 2015 destroy: function (dt, node, conf) { 2016 dt.off('length.dt' + conf.namespace); 2017 } 2018 }; 2019 }), 2020 init: function (dt, node, conf) { 2021 var that = this; 2022 dt.on('length.dt' + conf.namespace, function () { 2023 that.text(conf.text); 2024 }); 2025 }, 2026 destroy: function (dt, node, conf) { 2027 dt.off('length.dt' + conf.namespace); 2028 } 2029 }; 2030 }, 2031 spacer: { 2032 style: 'empty', 2033 spacer: true, 2034 text: function (dt) { 2035 return dt.i18n('buttons.spacer', ''); 2036 } 2037 } 2038}); 2039 2040/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 2041 * DataTables API 2042 * 2043 * For complete documentation, please refer to the docs/api directory or the 2044 * DataTables site 2045 */ 2046 2047// Buttons group and individual button selector 2048DataTable.Api.register('buttons()', function (group, selector) { 2049 // Argument shifting 2050 if (selector === undefined) { 2051 selector = group; 2052 group = undefined; 2053 } 2054 2055 this.selector.buttonGroup = group; 2056 2057 var res = this.iterator( 2058 true, 2059 'table', 2060 function (ctx) { 2061 if (ctx._buttons) { 2062 return Buttons.buttonSelector( 2063 Buttons.instanceSelector(group, ctx._buttons), 2064 selector 2065 ); 2066 } 2067 }, 2068 true 2069 ); 2070 2071 res._groupSelector = group; 2072 return res; 2073}); 2074 2075// Individual button selector 2076DataTable.Api.register('button()', function (group, selector) { 2077 // just run buttons() and truncate 2078 var buttons = this.buttons(group, selector); 2079 2080 if (buttons.length > 1) { 2081 buttons.splice(1, buttons.length); 2082 } 2083 2084 return buttons; 2085}); 2086 2087// Active buttons 2088DataTable.Api.registerPlural('buttons().active()', 'button().active()', function (flag) { 2089 if (flag === undefined) { 2090 return this.map(function (set) { 2091 return set.inst.active(set.node); 2092 }); 2093 } 2094 2095 return this.each(function (set) { 2096 set.inst.active(set.node, flag); 2097 }); 2098}); 2099 2100// Get / set button action 2101DataTable.Api.registerPlural('buttons().action()', 'button().action()', function (action) { 2102 if (action === undefined) { 2103 return this.map(function (set) { 2104 return set.inst.action(set.node); 2105 }); 2106 } 2107 2108 return this.each(function (set) { 2109 set.inst.action(set.node, action); 2110 }); 2111}); 2112 2113// Collection control 2114DataTable.Api.registerPlural( 2115 'buttons().collectionRebuild()', 2116 'button().collectionRebuild()', 2117 function (buttons) { 2118 return this.each(function (set) { 2119 for (var i = 0; i < buttons.length; i++) { 2120 if (typeof buttons[i] === 'object') { 2121 buttons[i].parentConf = set; 2122 } 2123 } 2124 set.inst.collectionRebuild(set.node, buttons); 2125 }); 2126 } 2127); 2128 2129// Enable / disable buttons 2130DataTable.Api.register(['buttons().enable()', 'button().enable()'], function (flag) { 2131 return this.each(function (set) { 2132 set.inst.enable(set.node, flag); 2133 }); 2134}); 2135 2136// Disable buttons 2137DataTable.Api.register(['buttons().disable()', 'button().disable()'], function () { 2138 return this.each(function (set) { 2139 set.inst.disable(set.node); 2140 }); 2141}); 2142 2143// Button index 2144DataTable.Api.register('button().index()', function () { 2145 var idx = null; 2146 2147 this.each(function (set) { 2148 var res = set.inst.index(set.node); 2149 2150 if (res !== null) { 2151 idx = res; 2152 } 2153 }); 2154 2155 return idx; 2156}); 2157 2158// Get button nodes 2159DataTable.Api.registerPlural('buttons().nodes()', 'button().node()', function () { 2160 var jq = $(); 2161 2162 // jQuery will automatically reduce duplicates to a single entry 2163 $( 2164 this.each(function (set) { 2165 jq = jq.add(set.inst.node(set.node)); 2166 }) 2167 ); 2168 2169 return jq; 2170}); 2171 2172// Get / set button processing state 2173DataTable.Api.registerPlural('buttons().processing()', 'button().processing()', function (flag) { 2174 if (flag === undefined) { 2175 return this.map(function (set) { 2176 return set.inst.processing(set.node); 2177 }); 2178 } 2179 2180 return this.each(function (set) { 2181 set.inst.processing(set.node, flag); 2182 }); 2183}); 2184 2185// Get / set button text (i.e. the button labels) 2186DataTable.Api.registerPlural('buttons().text()', 'button().text()', function (label) { 2187 if (label === undefined) { 2188 return this.map(function (set) { 2189 return set.inst.text(set.node); 2190 }); 2191 } 2192 2193 return this.each(function (set) { 2194 set.inst.text(set.node, label); 2195 }); 2196}); 2197 2198// Trigger a button's action 2199DataTable.Api.registerPlural('buttons().trigger()', 'button().trigger()', function () { 2200 return this.each(function (set) { 2201 set.inst.node(set.node).trigger('click'); 2202 }); 2203}); 2204 2205// Button resolver to the popover 2206DataTable.Api.register('button().popover()', function (content, options) { 2207 return this.map(function (set) { 2208 return set.inst._popover(content, this.button(this[0].node), options); 2209 }); 2210}); 2211 2212// Get the container elements 2213DataTable.Api.register('buttons().containers()', function () { 2214 var jq = $(); 2215 var groupSelector = this._groupSelector; 2216 2217 // We need to use the group selector directly, since if there are no buttons 2218 // the result set will be empty 2219 this.iterator(true, 'table', function (ctx) { 2220 if (ctx._buttons) { 2221 var insts = Buttons.instanceSelector(groupSelector, ctx._buttons); 2222 2223 for (var i = 0, ien = insts.length; i < ien; i++) { 2224 jq = jq.add(insts[i].container()); 2225 } 2226 } 2227 }); 2228 2229 return jq; 2230}); 2231 2232DataTable.Api.register('buttons().container()', function () { 2233 // API level of nesting is `buttons()` so we can zip into the containers method 2234 return this.containers().eq(0); 2235}); 2236 2237// Add a new button 2238DataTable.Api.register('button().add()', function (idx, conf, draw) { 2239 var ctx = this.context; 2240 2241 // Don't use `this` as it could be empty - select the instances directly 2242 if (ctx.length) { 2243 var inst = Buttons.instanceSelector(this._groupSelector, ctx[0]._buttons); 2244 2245 if (inst.length) { 2246 inst[0].add(conf, idx, draw); 2247 } 2248 } 2249 2250 return this.button(this._groupSelector, idx); 2251}); 2252 2253// Destroy the button sets selected 2254DataTable.Api.register('buttons().destroy()', function () { 2255 this.pluck('inst') 2256 .unique() 2257 .each(function (inst) { 2258 inst.destroy(); 2259 }); 2260 2261 return this; 2262}); 2263 2264// Remove a button 2265DataTable.Api.registerPlural('buttons().remove()', 'buttons().remove()', function () { 2266 this.each(function (set) { 2267 set.inst.remove(set.node); 2268 }); 2269 2270 return this; 2271}); 2272 2273// Information box that can be used by buttons 2274var _infoTimer; 2275DataTable.Api.register('buttons.info()', function (title, message, time) { 2276 var that = this; 2277 2278 if (title === false) { 2279 this.off('destroy.btn-info'); 2280 _fadeOut($('#datatables_buttons_info'), 400, function () { 2281 $(this).remove(); 2282 }); 2283 clearTimeout(_infoTimer); 2284 _infoTimer = null; 2285 2286 return this; 2287 } 2288 2289 if (_infoTimer) { 2290 clearTimeout(_infoTimer); 2291 } 2292 2293 if ($('#datatables_buttons_info').length) { 2294 $('#datatables_buttons_info').remove(); 2295 } 2296 2297 title = title ? '<h2>' + title + '</h2>' : ''; 2298 2299 _fadeIn( 2300 $('<div id="datatables_buttons_info" class="dt-button-info"/>') 2301 .html(title) 2302 .append($('<div/>')[typeof message === 'string' ? 'html' : 'append'](message)) 2303 .css('display', 'none') 2304 .appendTo('body') 2305 ); 2306 2307 if (time !== undefined && time !== 0) { 2308 _infoTimer = setTimeout(function () { 2309 that.buttons.info(false); 2310 }, time); 2311 } 2312 2313 this.on('destroy.btn-info', function () { 2314 that.buttons.info(false); 2315 }); 2316 2317 return this; 2318}); 2319 2320// Get data from the table for export - this is common to a number of plug-in 2321// buttons so it is included in the Buttons core library 2322DataTable.Api.register('buttons.exportData()', function (options) { 2323 if (this.context.length) { 2324 return _exportData(new DataTable.Api(this.context[0]), options); 2325 } 2326}); 2327 2328// Get information about the export that is common to many of the export data 2329// types (DRY) 2330DataTable.Api.register('buttons.exportInfo()', function (conf) { 2331 if (!conf) { 2332 conf = {}; 2333 } 2334 2335 return { 2336 filename: _filename(conf), 2337 title: _title(conf), 2338 messageTop: _message(this, conf.message || conf.messageTop, 'top'), 2339 messageBottom: _message(this, conf.messageBottom, 'bottom') 2340 }; 2341}); 2342 2343/** 2344 * Get the file name for an exported file. 2345 * 2346 * @param {object} config Button configuration 2347 * @param {boolean} incExtension Include the file name extension 2348 */ 2349var _filename = function (config) { 2350 // Backwards compatibility 2351 var filename = 2352 config.filename === '*' && 2353 config.title !== '*' && 2354 config.title !== undefined && 2355 config.title !== null && 2356 config.title !== '' 2357 ? config.title 2358 : config.filename; 2359 2360 if (typeof filename === 'function') { 2361 filename = filename(); 2362 } 2363 2364 if (filename === undefined || filename === null) { 2365 return null; 2366 } 2367 2368 if (filename.indexOf('*') !== -1) { 2369 filename = filename.replace('*', $('head > title').text()).trim(); 2370 } 2371 2372 // Strip characters which the OS will object to 2373 filename = filename.replace(/[^a-zA-Z0-9_\u00A1-\uFFFF\.,\-_ !\(\)]/g, ''); 2374 2375 var extension = _stringOrFunction(config.extension); 2376 if (!extension) { 2377 extension = ''; 2378 } 2379 2380 return filename + extension; 2381}; 2382 2383/** 2384 * Simply utility method to allow parameters to be given as a function 2385 * 2386 * @param {undefined|string|function} option Option 2387 * @return {null|string} Resolved value 2388 */ 2389var _stringOrFunction = function (option) { 2390 if (option === null || option === undefined) { 2391 return null; 2392 } 2393 else if (typeof option === 'function') { 2394 return option(); 2395 } 2396 return option; 2397}; 2398 2399/** 2400 * Get the title for an exported file. 2401 * 2402 * @param {object} config Button configuration 2403 */ 2404var _title = function (config) { 2405 var title = _stringOrFunction(config.title); 2406 2407 return title === null 2408 ? null 2409 : title.indexOf('*') !== -1 2410 ? title.replace('*', $('head > title').text() || 'Exported data') 2411 : title; 2412}; 2413 2414var _message = function (dt, option, position) { 2415 var message = _stringOrFunction(option); 2416 if (message === null) { 2417 return null; 2418 } 2419 2420 var caption = $('caption', dt.table().container()).eq(0); 2421 if (message === '*') { 2422 var side = caption.css('caption-side'); 2423 if (side !== position) { 2424 return null; 2425 } 2426 2427 return caption.length ? caption.text() : ''; 2428 } 2429 2430 return message; 2431}; 2432 2433var _exportTextarea = $('<textarea/>')[0]; 2434var _exportData = function (dt, inOpts) { 2435 var config = $.extend( 2436 true, 2437 {}, 2438 { 2439 rows: null, 2440 columns: '', 2441 modifier: { 2442 search: 'applied', 2443 order: 'applied' 2444 }, 2445 orthogonal: 'display', 2446 stripHtml: true, 2447 stripNewlines: true, 2448 decodeEntities: true, 2449 trim: true, 2450 format: { 2451 header: function (d) { 2452 return Buttons.stripData(d, config); 2453 }, 2454 footer: function (d) { 2455 return Buttons.stripData(d, config); 2456 }, 2457 body: function (d) { 2458 return Buttons.stripData(d, config); 2459 } 2460 }, 2461 customizeData: null 2462 }, 2463 inOpts 2464 ); 2465 2466 var header = dt 2467 .columns(config.columns) 2468 .indexes() 2469 .map(function (idx) { 2470 var el = dt.column(idx).header(); 2471 return config.format.header(el.innerHTML, idx, el); 2472 }) 2473 .toArray(); 2474 2475 var footer = dt.table().footer() 2476 ? dt 2477 .columns(config.columns) 2478 .indexes() 2479 .map(function (idx) { 2480 var el = dt.column(idx).footer(); 2481 return config.format.footer(el ? el.innerHTML : '', idx, el); 2482 }) 2483 .toArray() 2484 : null; 2485 2486 // If Select is available on this table, and any rows are selected, limit the export 2487 // to the selected rows. If no rows are selected, all rows will be exported. Specify 2488 // a `selected` modifier to control directly. 2489 var modifier = $.extend({}, config.modifier); 2490 if (dt.select && typeof dt.select.info === 'function' && modifier.selected === undefined) { 2491 if (dt.rows(config.rows, $.extend({ selected: true }, modifier)).any()) { 2492 $.extend(modifier, { selected: true }); 2493 } 2494 } 2495 2496 var rowIndexes = dt.rows(config.rows, modifier).indexes().toArray(); 2497 var selectedCells = dt.cells(rowIndexes, config.columns); 2498 var cells = selectedCells.render(config.orthogonal).toArray(); 2499 var cellNodes = selectedCells.nodes().toArray(); 2500 2501 var columns = header.length; 2502 var rows = columns > 0 ? cells.length / columns : 0; 2503 var body = []; 2504 var cellCounter = 0; 2505 2506 for (var i = 0, ien = rows; i < ien; i++) { 2507 var row = [columns]; 2508 2509 for (var j = 0; j < columns; j++) { 2510 row[j] = config.format.body(cells[cellCounter], i, j, cellNodes[cellCounter]); 2511 cellCounter++; 2512 } 2513 2514 body[i] = row; 2515 } 2516 2517 var data = { 2518 header: header, 2519 footer: footer, 2520 body: body 2521 }; 2522 2523 if (config.customizeData) { 2524 config.customizeData(data); 2525 } 2526 2527 return data; 2528}; 2529 2530/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 2531 * DataTables interface 2532 */ 2533 2534// Attach to DataTables objects for global access 2535$.fn.dataTable.Buttons = Buttons; 2536$.fn.DataTable.Buttons = Buttons; 2537 2538// DataTables creation - check if the buttons have been defined for this table, 2539// they will have been if the `B` option was used in `dom`, otherwise we should 2540// create the buttons instance here so they can be inserted into the document 2541// using the API. Listen for `init` for compatibility with pre 1.10.10, but to 2542// be removed in future. 2543$(document).on('init.dt plugin-init.dt', function (e, settings) { 2544 if (e.namespace !== 'dt') { 2545 return; 2546 } 2547 2548 var opts = settings.oInit.buttons || DataTable.defaults.buttons; 2549 2550 if (opts && !settings._buttons) { 2551 new Buttons(settings, opts).container(); 2552 } 2553}); 2554 2555function _init(settings, options) { 2556 var api = new DataTable.Api(settings); 2557 var opts = options ? options : api.init().buttons || DataTable.defaults.buttons; 2558 2559 return new Buttons(api, opts).container(); 2560} 2561 2562// DataTables `dom` feature option 2563DataTable.ext.feature.push({ 2564 fnInit: _init, 2565 cFeature: 'B' 2566}); 2567 2568// DataTables 2 layout feature 2569if (DataTable.ext.features) { 2570 DataTable.ext.features.register('buttons', _init); 2571} 2572 2573 2574return DataTable; 2575})); 2576