diff --git a/LICENSE b/LICENSE index d84d8e37e1c..00eb51888e3 100644 --- a/LICENSE +++ b/LICENSE @@ -301,28 +301,28 @@ Files (in server/monitor/src/main/resources/): Copyright 2019-2024 The Bootstrap Authors Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) -## DataTables 1.13.11 (https://datatables.net) +## DataTables 3.0.1 (https://datatables.net) Files (in server/monitor/src/main/resources/): * org/apache/accumulo/monitor/resources/external/datatables/**/* - Copyright (c) 2008-2024 SpryMedia Ltd + Copyright (c) SpryMedia Ltd - datatables.net/license Licensed under the MIT license (see above) -## DataTables Buttons 2.4.2 (https://datatables.net) +## DataTables Buttons 4.0.1 (https://datatables.net) Files (in server/monitor/src/main/resources/): * org/apache/accumulo/monitor/resources/external/datatables/**/* - Copyright (c) 2010-2015 SpryMedia Limited + Copyright (c) SpryMedia Ltd - datatables.net/license Licensed under the MIT license (see above) -## DataTables ColReorder 1.7.0 (https://datatables.net) +## DataTables ColReorder 3.0.0 (https://datatables.net) Files (in server/monitor/src/main/resources/): * org/apache/accumulo/monitor/resources/external/datatables/**/* - Copyright (c) 2010-2015 SpryMedia Limited + Copyright (c) SpryMedia Ltd - datatables.net/license Licensed under the MIT license (see above) ********** diff --git a/assemble/src/main/resources/LICENSE b/assemble/src/main/resources/LICENSE index d6bd358baf6..ec59b91ade0 100644 --- a/assemble/src/main/resources/LICENSE +++ b/assemble/src/main/resources/LICENSE @@ -301,28 +301,28 @@ Files (in lib/accumulo-monitor-*.jar): Copyright 2019-2024 The Bootstrap Authors Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) -## DataTables 1.13.11 (https://datatables.net) +## DataTables 3.0.1 (https://datatables.net) Files (in lib/accumulo-monitor-*.jar): * org/apache/accumulo/monitor/resources/external/datatables/**/* - Copyright (c) 2008-2024 SpryMedia Ltd + Copyright (c) SpryMedia Ltd - datatables.net/license Licensed under the MIT license (see above) -## DataTables Buttons 2.4.2 (https://datatables.net) +## DataTables Buttons 4.0.1 (https://datatables.net) Files (in server/monitor/src/main/resources/): * org/apache/accumulo/monitor/resources/external/datatables/**/* - Copyright (c) 2010-2015 SpryMedia Limited + Copyright (c) SpryMedia Ltd - datatables.net/license Licensed under the MIT license (see above) -## DataTables ColReorder 1.7.0 (https://datatables.net) +## DataTables ColReorder 3.0.0 (https://datatables.net) Files (in server/monitor/src/main/resources/): * org/apache/accumulo/monitor/resources/external/datatables/**/* - Copyright (c) 2010-2015 SpryMedia Limited + Copyright (c) SpryMedia Ltd - datatables.net/license Licensed under the MIT license (see above) ********** diff --git a/server/monitor/src/main/appended-resources/META-INF/LICENSE b/server/monitor/src/main/appended-resources/META-INF/LICENSE index 11f0d3ced7c..cdb027a85db 100644 --- a/server/monitor/src/main/appended-resources/META-INF/LICENSE +++ b/server/monitor/src/main/appended-resources/META-INF/LICENSE @@ -63,28 +63,28 @@ Files (in lib/accumulo-monitor-*.jar): Copyright 2019-2024 The Bootstrap Authors Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) -## DataTables 1.13.11 (https://datatables.net) +## DataTables 3.0.1 (https://datatables.net) Files (in lib/accumulo-monitor-*.jar): * org/apache/accumulo/monitor/resources/external/datatables/**/* - Copyright (c) 2008-2024 SpryMedia Ltd + Copyright (c) SpryMedia Ltd - datatables.net/license Licensed under the MIT license (see above) -## DataTables Buttons 2.4.2 (https://datatables.net) +## DataTables Buttons 4.0.1 (https://datatables.net) Files (in server/monitor/src/main/resources/): * org/apache/accumulo/monitor/resources/external/datatables/**/* - Copyright (c) 2010-2015 SpryMedia Limited + Copyright (c) SpryMedia Ltd - datatables.net/license Licensed under the MIT license (see above) -## DataTables ColReorder 1.7.0 (https://datatables.net) +## DataTables ColReorder 3.0.0 (https://datatables.net) Files (in server/monitor/src/main/resources/): * org/apache/accumulo/monitor/resources/external/datatables/**/* - Copyright (c) 2010-2015 SpryMedia Limited + Copyright (c) SpryMedia Ltd - datatables.net/license Licensed under the MIT license (see above) ********** diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/css/screen.css b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/css/screen.css index bef7ab65324..05a4a78eddb 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/css/screen.css +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/css/screen.css @@ -239,10 +239,6 @@ pre.logevent { margin-right: 0; } -.align-left { - float: left; -} - .error, .error a { color: #ffffff; background-color: #d9534f; diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/buttons.bootstrap5.css b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/buttons.bootstrap5.css index df249f01a1f..804b8021aff 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/buttons.bootstrap5.css +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/buttons.bootstrap5.css @@ -27,8 +27,9 @@ transform: rotate(360deg); } } -div.dataTables_wrapper { - position: relative; +:root { + --dtb-processing_border: 2px solid rgb(40, 40, 40); + --dtb-processing_filter: drop-shadow(0px 0px 3px white); } div.dt-buttons { @@ -37,6 +38,7 @@ div.dt-buttons { div.dt-buttons .dt-button { overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; } div.dt-button-info { @@ -80,10 +82,13 @@ button.dtb-hide-drop { div.dt-button-collection-title { text-align: center; - padding: 0.3em 0 0.5em; + padding: 0.3em 0.5em 0.5em; margin-left: 0.5em; margin-right: 0.5em; font-size: 0.9em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } div.dt-button-collection-title:empty { @@ -115,7 +120,7 @@ div.dt-button-collection .dt-button-active:after { margin-top: -10px; right: 1em; display: inline-block; - content: "✓"; + content: "\2713"; color: inherit; } div.dt-button-collection .dt-button-active.dt-button-split { @@ -133,7 +138,7 @@ div.dt-button-collection .dt-button-active.dt-button-split > *:first-child:after margin-top: -10px; right: 1em; display: inline-block; - content: "✓"; + content: "\2713"; color: inherit; } div.dt-button-collection .dt-button-active-a a { @@ -143,7 +148,7 @@ div.dt-button-collection .dt-button-active-a a:after { position: absolute; right: 1em; display: inline-block; - content: "✓"; + content: "\2713"; color: inherit; } div.dt-button-collection span.dt-button-spacer { @@ -162,21 +167,81 @@ div.dt-button-collection span.dt-button-spacer.bar { padding-left: 0; } -html.dark div.dt-button-info { - background-color: var(--dt-html-background); - border: 1px solid rgba(255, 255, 255, 0.15); +@media print { + table.dataTable tr > * { + box-shadow: none !important; + } +} +:root[data-bs-theme=dark] { + --dtb-processing_border: 2px solid rgb(224, 224, 224); + --dtb-processing_filter: drop-shadow(0px 0px 3px black); } div.dt-buttons div.btn-group { position: initial; } -div.dt-buttons div.dropdown-menu { +div.dt-buttons span.dt-button-spacer.empty { + margin: 1px; +} +div.dt-buttons span.dt-button-spacer.bar:empty { + height: inherit; +} +div.dt-buttons .btn.processing { + color: rgba(0, 0, 0, 0.2); +} +div.dt-buttons .btn.processing:after { + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + box-sizing: border-box; + display: block; + content: " "; + border: var(--dtb-processing_border); + border-radius: 50%; + border-left-color: transparent; + border-right-color: transparent; + animation: dtb-spinner 1500ms infinite linear; + -o-animation: dtb-spinner 1500ms infinite linear; + -ms-animation: dtb-spinner 1500ms infinite linear; + -webkit-animation: dtb-spinner 1500ms infinite linear; + -moz-animation: dtb-spinner 1500ms infinite linear; + filter: var(--dtb-processing_filter); +} + +div.dt-button-collection { + position: absolute; + min-width: 200px; margin-top: 4px; + z-index: 2002; + background-color: var(--bs-body-bg); + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + box-shadow: var(--bs-box-shadow); +} +div.dt-button-collection div.dt-button-collection-title { + padding: 0.75em 0 0.25em; } -div.dt-buttons div.dropdown-menu .dt-button { +div.dt-button-collection .dropdown-menu { position: relative; + display: block; + width: 100%; + background-color: transparent; + border: none; + border-radius: 0; + box-shadow: none; +} +div.dt-button-collection .dt-button { + position: relative; +} +div.dt-button-collection .dt-button.dropdown-toggle::after { + position: absolute; + right: 12px; + top: 14px; } -div.dt-buttons div.dropdown-menu div.dt-button-split { +div.dt-button-collection div.dt-button-split { display: flex; flex-direction: row; flex-wrap: wrap; @@ -184,12 +249,11 @@ div.dt-buttons div.dropdown-menu div.dt-button-split { align-content: flex-start; align-items: stretch; } -div.dt-buttons div.dropdown-menu div.dt-button-split a:first-child { +div.dt-button-collection div.dt-button-split > *:first-child { min-width: auto; flex: 1 0 50px; - padding-right: 0; } -div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child { +div.dt-button-collection div.dt-button-split button:last-child { min-width: 33px; flex: 0; background: transparent; @@ -197,16 +261,13 @@ div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child { line-height: 1rem; color: var(--bs-dropdown-link-color); padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); + overflow: visible; } -div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child:hover { +div.dt-button-collection div.dt-button-split button:last-child:hover { color: var(--bs-dropdown-link-hover-color); background-color: var(--bs-dropdown-link-hover-bg); } -div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child:after { - position: relative; - left: -3px; -} -div.dt-buttons div.dropdown-menu.fixed { +div.dt-button-collection.fixed { position: fixed; display: block; top: 50%; @@ -216,77 +277,73 @@ div.dt-buttons div.dropdown-menu.fixed { background-color: white; padding: 0.5em; } -div.dt-buttons div.dropdown-menu.fixed.two-column { +div.dt-button-collection.fixed.two-column { margin-left: -200px; } -div.dt-buttons div.dropdown-menu.fixed.three-column { +div.dt-button-collection.fixed.three-column { margin-left: -225px; } -div.dt-buttons div.dropdown-menu.fixed.four-column { +div.dt-button-collection.fixed.four-column { margin-left: -300px; } -div.dt-buttons div.dropdown-menu.fixed.columns { +div.dt-button-collection.fixed.columns { margin-left: -409px; } @media screen and (max-width: 1024px) { - div.dt-buttons div.dropdown-menu.fixed.columns { + div.dt-button-collection.fixed.columns { margin-left: -308px; } } @media screen and (max-width: 640px) { - div.dt-buttons div.dropdown-menu.fixed.columns { + div.dt-button-collection.fixed.columns { margin-left: -203px; } } @media screen and (max-width: 460px) { - div.dt-buttons div.dropdown-menu.fixed.columns { + div.dt-button-collection.fixed.columns { margin-left: -100px; } } -div.dt-buttons div.dropdown-menu.fixed > :last-child { +div.dt-button-collection.fixed > :last-child { max-height: 100vh; overflow: auto; } -div.dt-buttons div.dropdown-menu.two-column > :last-child, div.dt-buttons div.dropdown-menu.three-column > :last-child, div.dt-buttons div.dropdown-menu.four-column > :last-child { +div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child { display: block !important; - -webkit-column-gap: 8px; - -moz-column-gap: 8px; - -ms-column-gap: 8px; - -o-column-gap: 8px; column-gap: 8px; } -div.dt-buttons div.dropdown-menu.two-column > :last-child > *, div.dt-buttons div.dropdown-menu.three-column > :last-child > *, div.dt-buttons div.dropdown-menu.four-column > :last-child > * { +div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * { -webkit-column-break-inside: avoid; break-inside: avoid; } -div.dt-buttons div.dropdown-menu.two-column { +div.dt-button-collection.two-column { width: 400px; } -div.dt-buttons div.dropdown-menu.two-column > :last-child { +div.dt-button-collection.two-column > :last-child { padding-bottom: 1px; column-count: 2; } -div.dt-buttons div.dropdown-menu.three-column { +div.dt-button-collection.three-column { width: 450px; } -div.dt-buttons div.dropdown-menu.three-column > :last-child { +div.dt-button-collection.three-column > :last-child { padding-bottom: 1px; column-count: 3; } -div.dt-buttons div.dropdown-menu.four-column { +div.dt-button-collection.four-column { width: 600px; } -div.dt-buttons div.dropdown-menu.four-column > :last-child { +div.dt-button-collection.four-column > :last-child { padding-bottom: 1px; column-count: 4; } -div.dt-buttons div.dropdown-menu .dt-button { +div.dt-button-collection .dt-button { border-radius: 0; } -div.dt-buttons div.dropdown-menu.columns { +div.dt-button-collection.columns { width: auto; } -div.dt-buttons div.dropdown-menu.columns > :last-child { +div.dt-button-collection.columns > :last-child { display: flex; flex-wrap: wrap; justify-content: flex-start; @@ -295,70 +352,41 @@ div.dt-buttons div.dropdown-menu.columns > :last-child { width: 818px; padding-bottom: 1px; } -div.dt-buttons div.dropdown-menu.columns > :last-child .dt-button { +div.dt-button-collection.columns > :last-child .dt-button { min-width: 200px; flex: 0 1; margin: 0; } -div.dt-buttons div.dropdown-menu.columns.dtb-b3 > :last-child, div.dt-buttons div.dropdown-menu.columns.dtb-b2 > :last-child, div.dt-buttons div.dropdown-menu.columns.dtb-b1 > :last-child { +div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child { justify-content: space-between; } -div.dt-buttons div.dropdown-menu.columns.dtb-b3 .dt-button { +div.dt-button-collection.columns.dtb-b3 .dt-button { flex: 1 1 32%; } -div.dt-buttons div.dropdown-menu.columns.dtb-b2 .dt-button { +div.dt-button-collection.columns.dtb-b2 .dt-button { flex: 1 1 48%; } -div.dt-buttons div.dropdown-menu.columns.dtb-b1 .dt-button { +div.dt-button-collection.columns.dtb-b1 .dt-button { flex: 1 1 100%; } @media screen and (max-width: 1024px) { - div.dt-buttons div.dropdown-menu.columns > :last-child { + div.dt-button-collection.columns > :last-child { width: 612px; } } @media screen and (max-width: 640px) { - div.dt-buttons div.dropdown-menu.columns > :last-child { + div.dt-button-collection.columns > :last-child { width: 406px; } - div.dt-buttons div.dropdown-menu.columns.dtb-b3 .dt-button { + div.dt-button-collection.columns.dtb-b3 .dt-button { flex: 0 1 32%; } } @media screen and (max-width: 460px) { - div.dt-buttons div.dropdown-menu.columns > :last-child { + div.dt-button-collection.columns > :last-child { width: 200px; } } -div.dt-buttons span.dt-button-spacer.empty { - margin: 1px; -} -div.dt-buttons span.dt-button-spacer.bar:empty { - height: inherit; -} -div.dt-buttons .btn.processing { - color: rgba(0, 0, 0, 0.2); -} -div.dt-buttons .btn.processing:after { - position: absolute; - top: 50%; - left: 50%; - width: 16px; - height: 16px; - margin: -8px 0 0 -8px; - box-sizing: border-box; - display: block; - content: " "; - border: 2px solid rgb(40, 40, 40); - border-radius: 50%; - border-left-color: transparent; - border-right-color: transparent; - animation: dtb-spinner 1500ms infinite linear; - -o-animation: dtb-spinner 1500ms infinite linear; - -ms-animation: dtb-spinner 1500ms infinite linear; - -webkit-animation: dtb-spinner 1500ms infinite linear; - -moz-animation: dtb-spinner 1500ms infinite linear; -} div.dt-button-background { position: fixed; @@ -380,8 +408,12 @@ div.dt-button-background { float: none; } } -:root[data-bs-theme=dark] div.dropdown-menu.dt-button-collection.fixed { - background-color: rgb(33, 37, 41); - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 8px; +div.dt-button-info { + background-color: var(--bs-body-bg); + border: 1px solid var(--bs-border-color-translucent); +} + +:root[data-bs-theme=dark] div.dt-button-collection.fixed { + background-color: var(--bs-body-bg); + border: 1px solid var(--bs-border-color-translucent); } diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/colReorder.bootstrap5.css b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/colReorder.bootstrap5.css index fc2b916033c..e6454d24f81 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/colReorder.bootstrap5.css +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/colReorder.bootstrap5.css @@ -1,19 +1,24 @@ -table.DTCR_clonedTable.dataTable { +body.dtcr-dragging { + overflow-x: hidden; +} + +table.dtcr-cloned.dataTable { position: absolute !important; background-color: rgba(255, 255, 255, 0.7); z-index: 202; border-radius: 4px; } -div.DTCR_pointer { - width: 1px; - background-color: #0d6efd; - z-index: 201; +table.dataTable tbody tr td.dtcr-moving { + background-color: rgba(127, 127, 127, 0.15); +} +table.dataTable tbody tr td.dtcr-moving-first { + border-left: 1px solid #0d6efd; +} +table.dataTable tbody tr td.dtcr-moving-last { + border-right: 1px solid #0d6efd; } -html.dark table.DTCR_clonedTable.dataTable { +html.dark table.dtcr-cloned.dataTable { background-color: rgba(33, 33, 33, 0.9); } -html.dark div.DTCR_pointer { - background-color: rgb(13, 110, 253); -} diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/dataTables.bootstrap5.css b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/dataTables.bootstrap5.css index 45201559a47..6c9d9d89855 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/dataTables.bootstrap5.css +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/dataTables.bootstrap5.css @@ -1,95 +1,172 @@ -@charset "UTF-8"; +/*! DataTables Bootstrap 5 integration + * © SpryMedia Ltd - datatables.net/license + */ :root { - --dt-row-selected: 13, 110, 253; - --dt-row-selected-text: 255, 255, 255; - --dt-row-selected-link: 9, 10, 11; - --dt-row-stripe: 0, 0, 0; - --dt-row-hover: 0, 0, 0; - --dt-column-ordering: 0, 0, 0; - --dt-html-background: white; + --dt_background-selected: 13, 110, 253; + --dt_color-selected: 255, 255, 255; + --dt_link_color-selected: 228, 228, 228; + --dt-row_background: transparent; + --dt-row_background-selected: var(--dt_background-selected); + --dt-row-text_color-selected: var(--dt_color-selected); + --dt-row-link_color-selected: var(--dt_link_color-selected); + --dt-row_background-stripe: 0, 0, 0; + --dt-row_background-hover: 0, 0, 0; + --dt-column-ordering_background: 0, 0, 0; + --dt-header-cell_align-items: center; + --dt-header-cell_vertical-align: middle; + --dt-html_background: white; } :root.dark { - --dt-html-background: rgb(33, 37, 41); + --dt-html_background: rgb(33, 37, 41); } -table.dataTable td.dt-control { +table.dataTable tbody td.dt-control { text-align: center; cursor: pointer; } -table.dataTable td.dt-control:before { +table.dataTable tbody td.dt-control:before { display: inline-block; - color: rgba(0, 0, 0, 0.5); - content: "▶"; + box-sizing: border-box; + content: ""; + border-top: 5px solid transparent; + border-left: 10px solid rgba(0, 0, 0, 0.5); + border-bottom: 5px solid transparent; + border-right: 0px solid transparent; +} +table.dataTable tbody tr.dt-hasChild td.dt-control:before { + border-top: 10px solid rgba(0, 0, 0, 0.5); + border-left: 5px solid transparent; + border-bottom: 0px solid transparent; + border-right: 5px solid transparent; +} + +:root.dark table.dataTable td.dt-control:before, +:root[data-bs-theme=dark] table.dataTable td.dt-control:before, +:root[data-theme=dark] table.dataTable td.dt-control:before { + border-left-color: rgba(255, 255, 255, 0.5); } -table.dataTable tr.dt-hasChild td.dt-control:before { - content: "▼"; +:root.dark table.dataTable tr.dt-hasChild td.dt-control:before, +:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before, +:root[data-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before { + border-top-color: rgba(255, 255, 255, 0.5); + border-left-color: transparent; } -html.dark table.dataTable td.dt-control:before, -:root[data-bs-theme=dark] table.dataTable td.dt-control:before { - color: rgba(255, 255, 255, 0.5); +div.dt-scroll { + width: 100%; } -html.dark table.dataTable tr.dt-hasChild td.dt-control:before, -:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before { - color: rgba(255, 255, 255, 0.5); + +div.dt-scroll-body thead tr, +div.dt-scroll-body tfoot tr { + height: 0; +} +div.dt-scroll-body thead tr th, div.dt-scroll-body thead tr td, +div.dt-scroll-body tfoot tr th, +div.dt-scroll-body tfoot tr td { + height: 0 !important; + padding-top: 0px !important; + padding-bottom: 0px !important; + border-top-width: 0px !important; + border-bottom-width: 0px !important; +} +div.dt-scroll-body thead tr th div.dt-scroll-sizing, div.dt-scroll-body thead tr td div.dt-scroll-sizing, +div.dt-scroll-body tfoot tr th div.dt-scroll-sizing, +div.dt-scroll-body tfoot tr td div.dt-scroll-sizing { + height: 0 !important; + overflow: hidden !important; } -table.dataTable thead > tr > th.sorting, table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting_asc_disabled, table.dataTable thead > tr > th.sorting_desc_disabled, -table.dataTable thead > tr > td.sorting, -table.dataTable thead > tr > td.sorting_asc, -table.dataTable thead > tr > td.sorting_desc, -table.dataTable thead > tr > td.sorting_asc_disabled, -table.dataTable thead > tr > td.sorting_desc_disabled { - cursor: pointer; +/*! DataTables Bootstrap 5 integration + * © SpryMedia Ltd - datatables.net/license + */ +:root { + --dt-order-arrow_color: rgb(51, 51, 51); + --dt-order-arrow_color-current: rgb(51, 51, 51); + --dt-order-arrow-height: 7px; + --dt-order-arrow_opacity: 0.125; + --dt-order-arrow_opacity-current: 0.65; + --dt-order-arrow-width: 8px; + --dt-order-arrow-gap: 1px; + --dt-order-header_outline-hover: 2px solid rgba(0, 0, 0, 0.05); +} +:root.dark, :root[data-bs-theme=dark], :root[data-theme=dark] { + --dt-order-arrow_color: rgb(229, 233, 238); + --dt-order-arrow_color-current: rgb(229, 233, 238); + --dt-order-header_outline-hover: 2px solid rgba(255, 255, 255, 0.05); +} + +table.dataTable thead > tr > th:active, +table.dataTable thead > tr > td:active { + outline: none; +} +table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, +table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before { + bottom: calc(50% + var(--dt-order-arrow-gap)); + border-bottom: var(--dt-order-arrow-height) solid var(--dt-order-arrow_color); + border-left: calc(var(--dt-order-arrow-width) / 2) solid transparent; + border-right: calc(var(--dt-order-arrow-width) / 2) solid transparent; +} +table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after, +table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:after, +table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after { + top: calc(50% + 1px); + border-top: var(--dt-order-arrow-height) solid var(--dt-order-arrow_color); + border-left: calc(var(--dt-order-arrow-width) / 2) solid transparent; + border-right: calc(var(--dt-order-arrow-width) / 2) solid transparent; +} +table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order, +table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order, +table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order, +table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order, +table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order { position: relative; - padding-right: 26px; -} -table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:after, -table.dataTable thead > tr > td.sorting:before, -table.dataTable thead > tr > td.sorting:after, -table.dataTable thead > tr > td.sorting_asc:before, -table.dataTable thead > tr > td.sorting_asc:after, -table.dataTable thead > tr > td.sorting_desc:before, -table.dataTable thead > tr > td.sorting_desc:after, -table.dataTable thead > tr > td.sorting_asc_disabled:before, -table.dataTable thead > tr > td.sorting_asc_disabled:after, -table.dataTable thead > tr > td.sorting_desc_disabled:before, -table.dataTable thead > tr > td.sorting_desc_disabled:after { + width: var(--dt-order-arrow-width); + align-self: stretch; +} +table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after, +table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:before, +table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:after, +table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:before, +table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:after, +table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:after, +table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after { position: absolute; display: block; - opacity: 0.125; - right: 10px; - line-height: 9px; - font-size: 0.8em; -} -table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:before, -table.dataTable thead > tr > td.sorting:before, -table.dataTable thead > tr > td.sorting_asc:before, -table.dataTable thead > tr > td.sorting_desc:before, -table.dataTable thead > tr > td.sorting_asc_disabled:before, -table.dataTable thead > tr > td.sorting_desc_disabled:before { - bottom: 50%; - content: "▲"; - content: "▲"/""; -} -table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:after, -table.dataTable thead > tr > td.sorting:after, -table.dataTable thead > tr > td.sorting_asc:after, -table.dataTable thead > tr > td.sorting_desc:after, -table.dataTable thead > tr > td.sorting_asc_disabled:after, -table.dataTable thead > tr > td.sorting_desc_disabled:after { - top: 50%; - content: "▼"; - content: "▼"/""; -} -table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:after, -table.dataTable thead > tr > td.sorting_asc:before, -table.dataTable thead > tr > td.sorting_desc:after { - opacity: 0.6; + content: " "; + height: 0; + width: 0; + left: 0; + color: var(--dt-order-arrow_color); + opacity: var(--dt-order-arrow_opacity); +} +table.dataTable thead > tr > th.dt-orderable-asc, table.dataTable thead > tr > th.dt-orderable-desc, +table.dataTable thead > tr > td.dt-orderable-asc, +table.dataTable thead > tr > td.dt-orderable-desc { + cursor: pointer; } -table.dataTable thead > tr > th.sorting_desc_disabled:after, table.dataTable thead > tr > th.sorting_asc_disabled:before, -table.dataTable thead > tr > td.sorting_desc_disabled:after, -table.dataTable thead > tr > td.sorting_asc_disabled:before { +table.dataTable thead > tr > th.dt-orderable-asc:hover, table.dataTable thead > tr > th.dt-orderable-desc:hover, +table.dataTable thead > tr > td.dt-orderable-asc:hover, +table.dataTable thead > tr > td.dt-orderable-desc:hover { + outline: var(--dt-order-header_outline-hover); + outline-offset: -2px; +} +table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before { + border-bottom-color: var(--dt-order-arrow_color-current); + opacity: var(--dt-order-arrow_opacity-current); +} +table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after, +table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after { + border-top-color: var(--dt-order-arrow_color-current); + opacity: var(--dt-order-arrow_opacity-current); +} +table.dataTable thead > tr > th.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) .dt-column-order:empty, table.dataTable thead > tr > th.sorting_desc_disabled .dt-column-order:after, table.dataTable thead > tr > th.sorting_asc_disabled .dt-column-order:before, +table.dataTable thead > tr > td.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) .dt-column-order:empty, +table.dataTable thead > tr > td.sorting_desc_disabled .dt-column-order:after, +table.dataTable thead > tr > td.sorting_asc_disabled .dt-column-order:before { display: none; } table.dataTable thead > tr > th:active, @@ -97,52 +174,101 @@ table.dataTable thead > tr > td:active { outline: none; } -div.dataTables_scrollBody > table.dataTable > thead > tr > th:before, div.dataTables_scrollBody > table.dataTable > thead > tr > th:after, -div.dataTables_scrollBody > table.dataTable > thead > tr > td:before, -div.dataTables_scrollBody > table.dataTable > thead > tr > td:after { +table.dataTable thead > tr > th div.dt-column-header, +table.dataTable thead > tr > th div.dt-column-footer, +table.dataTable thead > tr > td div.dt-column-header, +table.dataTable thead > tr > td div.dt-column-footer, +table.dataTable tfoot > tr > th div.dt-column-header, +table.dataTable tfoot > tr > th div.dt-column-footer, +table.dataTable tfoot > tr > td div.dt-column-header, +table.dataTable tfoot > tr > td div.dt-column-footer { + display: flex; + justify-content: space-between; + align-items: var(--dt-header-cell_align-items); + gap: 4px; +} +table.dataTable thead > tr > th div.dt-column-header .dt-column-title, +table.dataTable thead > tr > th div.dt-column-footer .dt-column-title, +table.dataTable thead > tr > td div.dt-column-header .dt-column-title, +table.dataTable thead > tr > td div.dt-column-footer .dt-column-title, +table.dataTable tfoot > tr > th div.dt-column-header .dt-column-title, +table.dataTable tfoot > tr > th div.dt-column-footer .dt-column-title, +table.dataTable tfoot > tr > td div.dt-column-header .dt-column-title, +table.dataTable tfoot > tr > td div.dt-column-footer .dt-column-title { + flex-grow: 1; +} +table.dataTable thead > tr > th div.dt-column-header .dt-column-title:empty, +table.dataTable thead > tr > th div.dt-column-footer .dt-column-title:empty, +table.dataTable thead > tr > td div.dt-column-header .dt-column-title:empty, +table.dataTable thead > tr > td div.dt-column-footer .dt-column-title:empty, +table.dataTable tfoot > tr > th div.dt-column-header .dt-column-title:empty, +table.dataTable tfoot > tr > th div.dt-column-footer .dt-column-title:empty, +table.dataTable tfoot > tr > td div.dt-column-header .dt-column-title:empty, +table.dataTable tfoot > tr > td div.dt-column-footer .dt-column-title:empty { display: none; } -div.dataTables_processing { +div.dt-scroll-body > table.dataTable > thead > tr > th, +div.dt-scroll-body > table.dataTable > thead > tr > td { + overflow: hidden; +} + +:root.dark table.dataTable thead > tr > th.dt-orderable-asc:hover, :root.dark table.dataTable thead > tr > th.dt-orderable-desc:hover, +:root.dark table.dataTable thead > tr > td.dt-orderable-asc:hover, +:root.dark table.dataTable thead > tr > td.dt-orderable-desc:hover, +:root[data-bs-theme=dark] table.dataTable thead > tr > th.dt-orderable-asc:hover, +:root[data-bs-theme=dark] table.dataTable thead > tr > th.dt-orderable-desc:hover, +:root[data-bs-theme=dark] table.dataTable thead > tr > td.dt-orderable-asc:hover, +:root[data-bs-theme=dark] table.dataTable thead > tr > td.dt-orderable-desc:hover { + outline: var(--dt-order-header_outline-hover); +} + +/*! DataTables Bootstrap 5 integration + * © SpryMedia Ltd - datatables.net/license + */ +:root { + --dt-processing-circle_background: var(--dt_background-selected); +} + +div.dt-processing { position: absolute; top: 50%; left: 50%; width: 200px; margin-left: -100px; - margin-top: -26px; + margin-top: -22px; text-align: center; padding: 2px; z-index: 10; } -div.dataTables_processing > div:last-child { +div.dt-processing > div:last-child { position: relative; width: 80px; height: 15px; margin: 1em auto; } -div.dataTables_processing > div:last-child > div { +div.dt-processing > div:last-child > div { position: absolute; top: 0; width: 13px; height: 13px; border-radius: 50%; - background: rgb(13, 110, 253); - background: rgb(var(--dt-row-selected)); + background: rgb(var(--dt-processing-circle_background)); animation-timing-function: cubic-bezier(0, 1, 1, 0); } -div.dataTables_processing > div:last-child > div:nth-child(1) { +div.dt-processing > div:last-child > div:nth-child(1) { left: 8px; animation: datatables-loader-1 0.6s infinite; } -div.dataTables_processing > div:last-child > div:nth-child(2) { +div.dt-processing > div:last-child > div:nth-child(2) { left: 8px; animation: datatables-loader-2 0.6s infinite; } -div.dataTables_processing > div:last-child > div:nth-child(3) { +div.dt-processing > div:last-child > div:nth-child(3) { left: 32px; animation: datatables-loader-2 0.6s infinite; } -div.dataTables_processing > div:last-child > div:nth-child(4) { +div.dt-processing > div:last-child > div:nth-child(4) { left: 56px; animation: datatables-loader-3 0.6s infinite; } @@ -174,32 +300,73 @@ div.dataTables_processing > div:last-child > div:nth-child(4) { table.dataTable.nowrap th, table.dataTable.nowrap td { white-space: nowrap; } +table.dataTable th, +table.dataTable td { + box-sizing: border-box; +} +table.dataTable th.dt-type-numeric, table.dataTable th.dt-type-date, +table.dataTable td.dt-type-numeric, +table.dataTable td.dt-type-date { + text-align: right; +} +table.dataTable th.dt-type-numeric div.dt-column-header, +table.dataTable th.dt-type-numeric div.dt-column-footer, table.dataTable th.dt-type-date div.dt-column-header, +table.dataTable th.dt-type-date div.dt-column-footer, +table.dataTable td.dt-type-numeric div.dt-column-header, +table.dataTable td.dt-type-numeric div.dt-column-footer, +table.dataTable td.dt-type-date div.dt-column-header, +table.dataTable td.dt-type-date div.dt-column-footer { + flex-direction: row-reverse; +} table.dataTable th.dt-left, table.dataTable td.dt-left { text-align: left; } +table.dataTable th.dt-left div.dt-column-header, +table.dataTable th.dt-left div.dt-column-footer, +table.dataTable td.dt-left div.dt-column-header, +table.dataTable td.dt-left div.dt-column-footer { + flex-direction: row; +} table.dataTable th.dt-center, -table.dataTable td.dt-center, -table.dataTable td.dataTables_empty { +table.dataTable td.dt-center { text-align: center; } table.dataTable th.dt-right, table.dataTable td.dt-right { text-align: right; } +table.dataTable th.dt-right div.dt-column-header, +table.dataTable th.dt-right div.dt-column-footer, +table.dataTable td.dt-right div.dt-column-header, +table.dataTable td.dt-right div.dt-column-footer { + flex-direction: row-reverse; +} table.dataTable th.dt-justify, table.dataTable td.dt-justify { text-align: justify; } +table.dataTable th.dt-justify div.dt-column-header, +table.dataTable th.dt-justify div.dt-column-footer, +table.dataTable td.dt-justify div.dt-column-header, +table.dataTable td.dt-justify div.dt-column-footer { + flex-direction: row; +} table.dataTable th.dt-nowrap, table.dataTable td.dt-nowrap { white-space: nowrap; } +table.dataTable th.dt-empty, +table.dataTable td.dt-empty { + text-align: center; + vertical-align: top; +} table.dataTable thead th, table.dataTable thead td, table.dataTable tfoot th, table.dataTable tfoot td { text-align: left; + vertical-align: var(--dt-header-cell_vertical-align); } table.dataTable thead th.dt-head-left, table.dataTable thead td.dt-head-left, @@ -207,6 +374,16 @@ table.dataTable tfoot th.dt-head-left, table.dataTable tfoot td.dt-head-left { text-align: left; } +table.dataTable thead th.dt-head-left div.dt-column-header, +table.dataTable thead th.dt-head-left div.dt-column-footer, +table.dataTable thead td.dt-head-left div.dt-column-header, +table.dataTable thead td.dt-head-left div.dt-column-footer, +table.dataTable tfoot th.dt-head-left div.dt-column-header, +table.dataTable tfoot th.dt-head-left div.dt-column-footer, +table.dataTable tfoot td.dt-head-left div.dt-column-header, +table.dataTable tfoot td.dt-head-left div.dt-column-footer { + flex-direction: row; +} table.dataTable thead th.dt-head-center, table.dataTable thead td.dt-head-center, table.dataTable tfoot th.dt-head-center, @@ -219,12 +396,32 @@ table.dataTable tfoot th.dt-head-right, table.dataTable tfoot td.dt-head-right { text-align: right; } +table.dataTable thead th.dt-head-right div.dt-column-header, +table.dataTable thead th.dt-head-right div.dt-column-footer, +table.dataTable thead td.dt-head-right div.dt-column-header, +table.dataTable thead td.dt-head-right div.dt-column-footer, +table.dataTable tfoot th.dt-head-right div.dt-column-header, +table.dataTable tfoot th.dt-head-right div.dt-column-footer, +table.dataTable tfoot td.dt-head-right div.dt-column-header, +table.dataTable tfoot td.dt-head-right div.dt-column-footer { + flex-direction: row-reverse; +} table.dataTable thead th.dt-head-justify, table.dataTable thead td.dt-head-justify, table.dataTable tfoot th.dt-head-justify, table.dataTable tfoot td.dt-head-justify { text-align: justify; } +table.dataTable thead th.dt-head-justify div.dt-column-header, +table.dataTable thead th.dt-head-justify div.dt-column-footer, +table.dataTable thead td.dt-head-justify div.dt-column-header, +table.dataTable thead td.dt-head-justify div.dt-column-footer, +table.dataTable tfoot th.dt-head-justify div.dt-column-header, +table.dataTable tfoot th.dt-head-justify div.dt-column-footer, +table.dataTable tfoot td.dt-head-justify div.dt-column-header, +table.dataTable tfoot td.dt-head-justify div.dt-column-footer { + flex-direction: row; +} table.dataTable thead th.dt-head-nowrap, table.dataTable thead td.dt-head-nowrap, table.dataTable tfoot th.dt-head-nowrap, @@ -252,192 +449,193 @@ table.dataTable tbody td.dt-body-nowrap { white-space: nowrap; } -/*! Bootstrap 5 integration for DataTables - * - * ©2020 SpryMedia Ltd, all rights reserved. - * License: MIT datatables.net/license/mit - */ -table.dataTable { +:root { + --dt_background-selected: 13, 110, 253; +} + +:root[data-bs-theme=dark] { + --dt-row_background-hover: 255, 255, 255; + --dt-row_background-stripe: 255, 255, 255; + --dt-column-ordering_background: 255, 255, 255; +} + +table.table.dataTable { clear: both; - margin-top: 6px !important; - margin-bottom: 6px !important; - max-width: none !important; - border-collapse: separate !important; + margin-bottom: 0; + max-width: none; border-spacing: 0; } -table.dataTable td, -table.dataTable th { - -webkit-box-sizing: content-box; - box-sizing: content-box; +table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * { + box-shadow: none; } -table.dataTable td.dataTables_empty, -table.dataTable th.dataTables_empty { - text-align: center; +table.table.dataTable > :not(caption) > * > * { + background-color: var(--bs-table-bg); } -table.dataTable.nowrap th, -table.dataTable.nowrap td { - white-space: nowrap; +table.table.dataTable > tbody > tr { + background-color: var(--dt-row_background); } -table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * { - box-shadow: none; +table.table.dataTable > tbody > tr.selected > * { + box-shadow: inset 0 0 0 9999px rgb(var(--dt-row_background-selected)); + color: rgb(var(--dt-row-text_color-selected)); +} +table.table.dataTable > tbody > tr.selected a { + color: rgb(var(--dt-row-link_color-selected)); +} +table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * { + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row_background-stripe), 0.05); } -table.dataTable > tbody > tr { - background-color: transparent; +table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1).selected > * { + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row_background-selected), 0.95); } -table.dataTable > tbody > tr.selected > * { - box-shadow: inset 0 0 0 9999px rgb(13, 110, 253); - box-shadow: inset 0 0 0 9999px rgb(var(--dt-row-selected)); - color: rgb(255, 255, 255); - color: rgb(var(--dt-row-selected-text)); +table.table.dataTable.table-hover > tbody > tr:hover > * { + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row_background-hover), 0.075); } -table.dataTable > tbody > tr.selected a { - color: rgb(9, 10, 11); - color: rgb(var(--dt-row-selected-link)); +table.table.dataTable.table-hover > tbody > tr.selected:hover > * { + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row_background-selected), 0.975); } -table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * { - box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.05); + +div.dt-container div.dt-layout-start > *:not(:last-child) { + margin-right: 1em; +} +div.dt-container div.dt-layout-end > *:not(:first-child) { + margin-left: 1em; } -table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1).selected > * { - box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95); - box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95); +div.dt-container div.dt-layout-full { + width: 100%; } -table.dataTable.table-hover > tbody > tr:hover > * { - box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.075); +div.dt-container div.dt-layout-full > *:only-child { + margin-left: auto; + margin-right: auto; } -table.dataTable.table-hover > tbody > tr.selected:hover > * { - box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975); - box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975); +div.dt-container div.dt-layout-table > div { + display: block !important; } -div.dataTables_wrapper div.dataTables_length label { +@media screen and (max-width: 767px) { + div.dt-container div.dt-layout-start > *:not(:last-child) { + margin-right: 0; + } + div.dt-container div.dt-layout-end > *:not(:first-child) { + margin-left: 0; + } +} +div.dt-container { + position: relative; +} +div.dt-container div.dt-length label { font-weight: normal; text-align: left; white-space: nowrap; } -div.dataTables_wrapper div.dataTables_length select { +div.dt-container div.dt-length select { width: auto; display: inline-block; + margin-right: 0.5em; } -div.dataTables_wrapper div.dataTables_filter { +div.dt-container div.dt-search { text-align: right; } -div.dataTables_wrapper div.dataTables_filter label { +div.dt-container div.dt-search label { font-weight: normal; white-space: nowrap; text-align: left; } -div.dataTables_wrapper div.dataTables_filter input { +div.dt-container div.dt-search input { margin-left: 0.5em; display: inline-block; width: auto; } -div.dataTables_wrapper div.dataTables_info { - padding-top: 0.85em; -} -div.dataTables_wrapper div.dataTables_paginate { +div.dt-container div.dt-paging { margin: 0; - white-space: nowrap; - text-align: right; } -div.dataTables_wrapper div.dataTables_paginate ul.pagination { +div.dt-container div.dt-paging ul.pagination { margin: 2px 0; - white-space: nowrap; - justify-content: flex-end; + flex-wrap: wrap; } -div.dataTables_wrapper div.dt-row { +div.dt-container div.dt-row { position: relative; } -div.dataTables_scrollHead table.dataTable { +div.dt-scroll-head table.dataTable { margin-bottom: 0 !important; } -div.dataTables_scrollBody > table { +div.dt-scroll-body { + border-bottom-color: var(--bs-border-color); + border-bottom-width: var(--bs-border-width); + border-bottom-style: solid; +} +div.dt-scroll-body > table { border-top: none; margin-top: 0 !important; margin-bottom: 0 !important; } -div.dataTables_scrollBody > table > thead .sorting:before, -div.dataTables_scrollBody > table > thead .sorting_asc:before, -div.dataTables_scrollBody > table > thead .sorting_desc:before, -div.dataTables_scrollBody > table > thead .sorting:after, -div.dataTables_scrollBody > table > thead .sorting_asc:after, -div.dataTables_scrollBody > table > thead .sorting_desc:after { - display: none; +div.dt-scroll-body > table > tbody > tr:first-child { + border-top-width: 0; } -div.dataTables_scrollBody > table > tbody tr:first-child th, -div.dataTables_scrollBody > table > tbody tr:first-child td { - border-top: none; +div.dt-scroll-body > table > thead > tr { + border-width: 0 !important; +} +div.dt-scroll-body > table > tbody > tr:last-child > * { + border-bottom: none; } -div.dataTables_scrollFoot > .dataTables_scrollFootInner { +div.dt-scroll-foot > .dt-scroll-footInner { box-sizing: content-box; } -div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { +div.dt-scroll-foot > .dt-scroll-footInner > table { margin-top: 0 !important; border-top: none; } +div.dt-scroll-foot > .dt-scroll-footInner > table > tfoot > tr:first-child { + border-top-width: 0 !important; +} @media screen and (max-width: 767px) { - div.dataTables_wrapper div.dataTables_length, - div.dataTables_wrapper div.dataTables_filter, - div.dataTables_wrapper div.dataTables_info, - div.dataTables_wrapper div.dataTables_paginate { + div.dt-container div.dt-length, + div.dt-container div.dt-search, + div.dt-container div.dt-info, + div.dt-container div.dt-paging { text-align: center; } - div.dataTables_wrapper div.dataTables_paginate ul.pagination { + div.dt-container .row { + --bs-gutter-y: 0.5rem; + } + div.dt-container div.dt-paging ul.pagination { justify-content: center !important; } } -table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled) { - padding-right: 20px; -} -table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled):before, table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled):after { - right: 5px; -} - -table.table-bordered.dataTable { - border-right-width: 0; -} -table.table-bordered.dataTable thead tr:first-child th, -table.table-bordered.dataTable thead tr:first-child td { - border-top-width: 1px; -} -table.table-bordered.dataTable th, -table.table-bordered.dataTable td { - border-left-width: 0; -} -table.table-bordered.dataTable th:first-child, table.table-bordered.dataTable th:first-child, -table.table-bordered.dataTable td:first-child, -table.table-bordered.dataTable td:first-child { - border-left-width: 1px; -} -table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child, -table.table-bordered.dataTable td:last-child, -table.table-bordered.dataTable td:last-child { - border-right-width: 1px; -} -table.table-bordered.dataTable th, -table.table-bordered.dataTable td { - border-bottom-width: 1px; +table.dataTable.table-sm > thead > tr th.dt-orderable-asc, table.dataTable.table-sm > thead > tr th.dt-orderable-desc, table.dataTable.table-sm > thead > tr th.dt-ordering-asc, table.dataTable.table-sm > thead > tr th.dt-ordering-desc, +table.dataTable.table-sm > thead > tr td.dt-orderable-asc, +table.dataTable.table-sm > thead > tr td.dt-orderable-desc, +table.dataTable.table-sm > thead > tr td.dt-ordering-asc, +table.dataTable.table-sm > thead > tr td.dt-ordering-desc { + padding-right: 0.25rem; +} +table.dataTable.table-sm > thead > tr th.dt-orderable-asc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-orderable-asc .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-orderable-desc .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-ordering-asc .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-ordering-desc .dt-column-order { + right: 0.25rem; +} +table.dataTable.table-sm > thead > tr th.dt-type-date .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-type-numeric .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-type-date .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-type-numeric .dt-column-order { + left: 0.25rem; } -div.dataTables_scrollHead table.table-bordered { +div.dt-scroll-head table.table-bordered { border-bottom-width: 0; } -div.table-responsive > div.dataTables_wrapper > div.row { - margin: 0; +div.table-responsive > div.dt-container > div.row { + margin-left: 0; + margin-right: 0; } -div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:first-child { +div.table-responsive > div.dt-container > div.row > div[class^=col-]:first-child { padding-left: 0; } -div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:last-child { +div.table-responsive > div.dt-container > div.row > div[class^=col-]:last-child { padding-right: 0; } - -:root[data-bs-theme=dark] { - --dt-row-hover: 255, 255, 255; - --dt-row-stripe: 255, 255, 255; - --dt-column-ordering: 255, 255, 255; -} diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.bootstrap5.js b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.bootstrap5.js index e3f41e8c0ec..27e0ab84b85 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.bootstrap5.js +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.bootstrap5.js @@ -1,117 +1,121 @@ -/*! Bootstrap integration for DataTables' Buttons - * © SpryMedia Ltd - datatables.net/license +/*! Buttons Bootstrap 5 styling 4.0.1 for DataTables + * Copyright (c) SpryMedia Ltd - datatables.net/license */ -(function( factory ){ - if ( typeof define === 'function' && define.amd ) { +(function(factory){ + if (typeof define === 'function' && define.amd) { // AMD - define( ['jquery', 'datatables.net-bs5', 'datatables.net-buttons'], function ( $ ) { - return factory( $, window, document ); - } ); + define(['datatables.net-bs5', 'datatables.net-buttons'], function (dt) { + return factory(window, document, dt); + }); } - else if ( typeof exports === 'object' ) { + else if (typeof exports === 'object') { // CommonJS - var jq = require('jquery'); - var cjsRequires = function (root, $) { - if ( ! $.fn.dataTable ) { - require('datatables.net-bs5')(root, $); + var cjsRequires = function (root) { + if (! root.DataTable) { + require('datatables.net-bs5')(root); } - if ( ! $.fn.dataTable.Buttons ) { - require('datatables.net-buttons')(root, $); + if (! window.DataTable.Buttons) { + require('datatables.net-buttons')(root); } }; if (typeof window === 'undefined') { - module.exports = function (root, $) { - if ( ! root ) { + 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 ); + cjsRequires(root); + return factory(root, root.document, root.DataTable); }; } else { - cjsRequires( window, jq ); - module.exports = factory( jq, window, window.document ); + cjsRequires(window); + module.exports = factory(window, window.document, window.DataTable); } } else { // Browser - factory( jQuery, window, document ); + factory(window, document, window.DataTable); } -}(function( $, window, document, undefined ) { -'use strict'; -var DataTable = $.fn.dataTable; +}(function(window, document, DataTable) { + 'use strict'; -$.extend(true, DataTable.Buttons.defaults, { - dom: { - container: { - className: 'dt-buttons btn-group flex-wrap' - }, - button: { - className: 'btn btn-secondary', - active: 'active' - }, - collection: { - action: { - dropHtml: '' - }, + var Dom = DataTable.Dom; + var util = DataTable.util; + + util.object.assignDeep(DataTable.Buttons.defaults, { + dom: { container: { - tag: 'div', - className: 'dropdown-menu dt-button-collection' + className: 'dt-buttons btn-group flex-wrap' }, - closeButton: false, button: { - tag: 'a', - className: 'dt-button dropdown-item', - active: 'dt-button-active', - disabled: 'disabled', - spacer: { - className: 'dropdown-divider', - tag: 'hr' - } - } - }, - split: { - action: { - tag: 'a', - className: 'btn btn-secondary dt-button-split-drop-button', - closeButton: false - }, - dropdown: { - tag: 'button', + className: 'btn btn-secondary', + active: 'active', dropHtml: '', - className: - 'btn btn-secondary dt-button-split-drop dropdown-toggle dropdown-toggle-split', + dropClass: 'dropdown-toggle' + }, + collection: { + container: { + tag: 'div', + className: 'dt-button-collection', + content: { + tag: 'ul', + className: 'dropdown-menu show' + } + }, closeButton: false, - align: 'split-left', - splitAlignClass: 'dt-button-split-left' + button: { + tag: 'li', + className: 'dt-button', + active: 'dt-button-active-a', + disabled: 'disabled', + liner: { + tag: 'a', + className: 'dropdown-item' + }, + spacer: { + className: 'divider', + tag: 'li' + } + } }, - wrapper: { - tag: 'div', - className: 'dt-button-split btn-group', - closeButton: false + split: { + action: { + tag: 'a', + className: 'btn btn-secondary dt-button-split-drop-button', + closeButton: false + }, + dropdown: { + tag: 'button', + className: + 'btn btn-secondary dt-button-split-drop dropdown-toggle-split', + closeButton: false, + align: 'split-left', + splitAlignClass: 'dt-button-split-left' + }, + wrapper: { + tag: 'div', + className: 'dt-button-split btn-group', + closeButton: false + } } + }, + buttonCreated: function (config, button, inCollection) { + return config.buttons && ! inCollection + ? Dom.c('div').classAdd('btn-group a').append(button) + : button; } - }, - buttonCreated: function (config, button) { - return config.buttons ? $('
').append(button) : button; - } -}); + }); -DataTable.ext.buttons.collection.className += ' dropdown-toggle'; -DataTable.ext.buttons.collection.rightAlignClassName = 'dropdown-menu-right'; + DataTable.ext.buttons.collection.rightAlignClassName = 'dropdown-menu-right'; -return DataTable; + return DataTable; })); diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.colVis.js b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.colVis.js index 7e8491bd1ee..894de4d8993 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.colVis.js +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.colVis.js @@ -1,260 +1,50 @@ -/*! - * Column visibility buttons for Buttons and DataTables. - * © SpryMedia Ltd - datatables.net/license - */ -(function( factory ){ - if ( typeof define === 'function' && define.amd ) { + +(function(factory){ + if (typeof define === 'function' && define.amd) { // AMD - define( ['jquery', 'datatables.net', 'datatables.net-buttons'], function ( $ ) { - return factory( $, window, document ); - } ); + define(['datatables.net', 'datatables.net-buttons'], function (dt) { + return factory(window, document, dt); + }); } - else if ( typeof exports === 'object' ) { + else if (typeof exports === 'object') { // CommonJS - var jq = require('jquery'); - var cjsRequires = function (root, $) { - if ( ! $.fn.dataTable ) { - require('datatables.net')(root, $); + var cjsRequires = function (root) { + if (! root.DataTable) { + require('datatables.net')(root); } - if ( ! $.fn.dataTable.Buttons ) { - require('datatables.net-buttons')(root, $); + if (! window.DataTable.Buttons) { + require('datatables.net-buttons')(root); } }; if (typeof window === 'undefined') { - module.exports = function (root, $) { - if ( ! root ) { + 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 ); + cjsRequires(root); + return factory(root, root.document, root.DataTable); }; } else { - cjsRequires( window, jq ); - module.exports = factory( jq, window, window.document ); + cjsRequires(window); + module.exports = factory(window, window.document, window.DataTable); } } else { // Browser - factory( jQuery, window, document ); + factory(window, document, window.DataTable); } -}(function( $, window, document, undefined ) { -'use strict'; -var DataTable = $.fn.dataTable; - - - -$.extend(DataTable.ext.buttons, { - // A collection of column visibility buttons - colvis: function (dt, conf) { - var node = null; - var buttonConf = { - extend: 'collection', - init: function (dt, n) { - node = n; - }, - text: function (dt) { - return dt.i18n('buttons.colvis', 'Column visibility'); - }, - className: 'buttons-colvis', - closeButton: false, - buttons: [ - { - extend: 'columnsToggle', - columns: conf.columns, - columnText: conf.columnText - } - ] - }; - - // Rebuild the collection with the new column structure if columns are reordered - dt.on('column-reorder.dt' + conf.namespace, function (e, settings, details) { - dt.button(null, dt.button(null, node).node()).collectionRebuild([ - { - extend: 'columnsToggle', - columns: conf.columns, - columnText: conf.columnText - } - ]); - }); - - return buttonConf; - }, - - // Selected columns with individual buttons - toggle column visibility - columnsToggle: function (dt, conf) { - var columns = dt - .columns(conf.columns) - .indexes() - .map(function (idx) { - return { - extend: 'columnToggle', - columns: idx, - columnText: conf.columnText - }; - }) - .toArray(); - - return columns; - }, - - // Single button to toggle column visibility - columnToggle: function (dt, conf) { - return { - extend: 'columnVisibility', - columns: conf.columns, - columnText: conf.columnText - }; - }, - - // Selected columns with individual buttons - set column visibility - columnsVisibility: function (dt, conf) { - var columns = dt - .columns(conf.columns) - .indexes() - .map(function (idx) { - return { - extend: 'columnVisibility', - columns: idx, - visibility: conf.visibility, - columnText: conf.columnText - }; - }) - .toArray(); - - return columns; - }, - - // Single button to set column visibility - columnVisibility: { - columns: undefined, // column selector - text: function (dt, button, conf) { - return conf._columnText(dt, conf); - }, - className: 'buttons-columnVisibility', - action: function (e, dt, button, conf) { - var col = dt.columns(conf.columns); - var curr = col.visible(); - - col.visible( - conf.visibility !== undefined ? conf.visibility : !(curr.length ? curr[0] : false) - ); - }, - init: function (dt, button, conf) { - var that = this; - button.attr('data-cv-idx', conf.columns); - - dt.on('column-visibility.dt' + conf.namespace, function (e, settings) { - if (!settings.bDestroying && settings.nTable == dt.settings()[0].nTable) { - that.active(dt.column(conf.columns).visible()); - } - }).on('column-reorder.dt' + conf.namespace, function (e, settings, details) { - // Button has been removed from the DOM - if (conf.destroying) { - return; - } - - if (dt.columns(conf.columns).count() !== 1) { - return; - } - - // This button controls the same column index but the text for the column has - // changed - that.text(conf._columnText(dt, conf)); - - // Since its a different column, we need to check its visibility - that.active(dt.column(conf.columns).visible()); - }); - - this.active(dt.column(conf.columns).visible()); - }, - destroy: function (dt, button, conf) { - dt.off('column-visibility.dt' + conf.namespace).off( - 'column-reorder.dt' + conf.namespace - ); - }, - - _columnText: function (dt, conf) { - // Use DataTables' internal data structure until this is presented - // is a public API. The other option is to use - // `$( column(col).node() ).text()` but the node might not have been - // populated when Buttons is constructed. - var idx = dt.column(conf.columns).index(); - var title = dt.settings()[0].aoColumns[idx].sTitle; - - if (!title) { - title = dt.column(idx).header().innerHTML; - } - - title = title - .replace(/\n/g, ' ') // remove new lines - .replace(//gi, ' ') // replace line breaks with spaces - .replace(//g, '') // remove select tags, including options text - .replace(//g, '') // strip HTML comments - .replace(/<.*?>/g, '') // strip HTML - .replace(/^\s+|\s+$/g, ''); // trim - - return conf.columnText ? conf.columnText(dt, idx, title) : title; - } - }, +}(function(window, document, DataTable) { + 'use strict'; - colvisRestore: { - className: 'buttons-colvisRestore', - - text: function (dt) { - return dt.i18n('buttons.colvisRestore', 'Restore visibility'); - }, - - init: function (dt, button, conf) { - conf._visOriginal = dt - .columns() - .indexes() - .map(function (idx) { - return dt.column(idx).visible(); - }) - .toArray(); - }, - - action: function (e, dt, button, conf) { - dt.columns().every(function (i) { - // Take into account that ColReorder might have disrupted our - // indexes - var idx = - dt.colReorder && dt.colReorder.transpose - ? dt.colReorder.transpose(i, 'toOriginal') - : i; - - this.visible(conf._visOriginal[idx]); - }); - } - }, - - colvisGroup: { - className: 'buttons-colvisGroup', - - action: function (e, dt, button, conf) { - dt.columns(conf.show).visible(true, false); - dt.columns(conf.hide).visible(false, false); - - dt.columns.adjust(); - }, - - show: [], - - hide: [] - } -}); +// File intentionally empty -return DataTable; + return DataTable; })); diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/colReorder.bootstrap5.js b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/colReorder.bootstrap5.js index 149f7d2f7a6..e209b7b698c 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/colReorder.bootstrap5.js +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/colReorder.bootstrap5.js @@ -1,58 +1,52 @@ -/*! Bootstrap 5 styling wrapper for ColReorder - * © SpryMedia Ltd - datatables.net/license +/*! ColReorder Bootstrap 5 styling 3.0.0 for DataTables + * Copyright (c) SpryMedia Ltd - datatables.net/license */ -(function( factory ){ - if ( typeof define === 'function' && define.amd ) { +(function(factory){ + if (typeof define === 'function' && define.amd) { // AMD - define( ['jquery', 'datatables.net-bs5', 'datatables.net-colreorder'], function ( $ ) { - return factory( $, window, document ); - } ); + define(['datatables.net-bs5', 'datatables.net-colreorder'], function (dt) { + return factory(window, document, dt); + }); } - else if ( typeof exports === 'object' ) { + else if (typeof exports === 'object') { // CommonJS - var jq = require('jquery'); - var cjsRequires = function (root, $) { - if ( ! $.fn.dataTable ) { - require('datatables.net-bs5')(root, $); + var cjsRequires = function (root) { + if (! root.DataTable) { + require('datatables.net-bs5')(root); } - if ( ! $.fn.dataTable.ColReorder ) { - require('datatables.net-colreorder')(root, $); + if (! window.DataTable.ColReorder) { + require('datatables.net-colreorder')(root); } }; if (typeof window === 'undefined') { - module.exports = function (root, $) { - if ( ! root ) { + 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 ); + cjsRequires(root); + return factory(root, root.document, root.DataTable); }; } else { - cjsRequires( window, jq ); - module.exports = factory( jq, window, window.document ); + cjsRequires(window); + module.exports = factory(window, window.document, window.DataTable); } } else { // Browser - factory( jQuery, window, document ); + factory(window, document, window.DataTable); } -}(function( $, window, document, undefined ) { -'use strict'; -var DataTable = $.fn.dataTable; +}(function(window, document, DataTable) { + 'use strict'; -return DataTable; + return DataTable; })); diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/dataTables.bootstrap5.js b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/dataTables.bootstrap5.js index cc375f0fb81..35b652aa517 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/dataTables.bootstrap5.js +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/dataTables.bootstrap5.js @@ -1,212 +1,102 @@ /*! DataTables Bootstrap 5 integration - * 2020 SpryMedia Ltd - datatables.net/license + * © SpryMedia Ltd - datatables.net/license */ -(function( factory ){ - if ( typeof define === 'function' && define.amd ) { +(function(factory){ + if (typeof define === 'function' && define.amd) { // AMD - define( ['jquery', 'datatables.net'], function ( $ ) { - return factory( $, window, document ); - } ); + define(['datatables.net'], function (dt) { + return factory(window, document, dt); + }); } - else if ( typeof exports === 'object' ) { + else if (typeof exports === 'object') { // CommonJS - var jq = require('jquery'); - var cjsRequires = function (root, $) { - if ( ! $.fn.dataTable ) { - require('datatables.net')(root, $); + var cjsRequires = function (root) { + if (! root.DataTable) { + require('datatables.net')(root); } }; if (typeof window === 'undefined') { - module.exports = function (root, $) { - if ( ! root ) { + 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 ); + cjsRequires(root); + return factory(root, root.document, root.DataTable); }; } else { - cjsRequires( window, jq ); - module.exports = factory( jq, window, window.document ); + cjsRequires(window); + module.exports = factory(window, window.document, window.DataTable); } } else { // Browser - factory( jQuery, window, document ); + factory(window, document, window.DataTable); } -}(function( $, window, document, undefined ) { -'use strict'; -var DataTable = $.fn.dataTable; - - - -/** - * DataTables integration for Bootstrap 5. This requires Bootstrap 5 and - * DataTables 1.10 or newer. - * - * This file sets the defaults and adds options to DataTables to style its - * controls using Bootstrap. See https://datatables.net/manual/styling/bootstrap - * for further information. - */ - -/* Set the defaults for DataTables initialisation */ -$.extend( true, DataTable.defaults, { - dom: - "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" + - "<'row dt-row'<'col-sm-12'tr>>" + - "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", - renderer: 'bootstrap' -} ); - - -/* Default class modification */ -$.extend( DataTable.ext.classes, { - sWrapper: "dataTables_wrapper dt-bootstrap5", - sFilterInput: "form-control form-control-sm", - sLengthSelect: "form-select form-select-sm", - sProcessing: "dataTables_processing card", - sPageButton: "paginate_button page-item" -} ); - - -/* Bootstrap paging button renderer */ -DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) { - var api = new DataTable.Api( settings ); - var classes = settings.oClasses; - var lang = settings.oLanguage.oPaginate; - var aria = settings.oLanguage.oAria.paginate || {}; - var btnDisplay, btnClass; - - var attach = function( container, buttons ) { - var i, ien, node, button; - var clickHandler = function ( e ) { - e.preventDefault(); - if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) { - api.page( e.data.action ).draw( 'page' ); - } - }; - - for ( i=0, ien=buttons.length ; i 0 ? - '' : ' disabled'); - break; - - case 'previous': - btnDisplay = lang.sPrevious; - btnClass = button + (page > 0 ? - '' : ' disabled'); - break; - - case 'next': - btnDisplay = lang.sNext; - btnClass = button + (page < pages-1 ? - '' : ' disabled'); - break; - - case 'last': - btnDisplay = lang.sLast; - btnClass = button + (page < pages-1 ? - '' : ' disabled'); - break; - - default: - btnDisplay = button + 1; - btnClass = page === button ? - 'active' : ''; - break; - } - - if ( btnDisplay ) { - var disabled = btnClass.indexOf('disabled') !== -1; - - node = $('
  • ', { - 'class': classes.sPageButton+' '+btnClass, - 'id': idx === 0 && typeof button === 'string' ? - settings.sTableId +'_'+ button : - null - } ) - .append( $('', { - 'href': disabled ? null : '#', - 'aria-controls': settings.sTableId, - 'aria-disabled': disabled ? 'true' : null, - 'aria-label': aria[ button ], - 'role': 'link', - 'aria-current': btnClass === 'active' ? 'page' : null, - 'data-dt-idx': button, - 'tabindex': disabled ? -1 : settings.iTabIndex, - 'class': 'page-link' - } ) - .html( btnDisplay ) - ) - .appendTo( container ); - - settings.oApi._fnBindAction( - node, {action: button}, clickHandler - ); - } - } +}(function(window, document, DataTable) { + 'use strict'; + + + /* Set the defaults for DataTables initialisation */ + DataTable.util.object.assignDeep(DataTable.defaults, { + renderer: 'bootstrap' + }); + /* Default class modification */ + DataTable.util.object.assignDeep(DataTable.ext.classes, { + container: 'dt-container dt-bootstrap5', + search: { + input: 'form-control form-control-sm' + }, + length: { + select: 'form-select form-select-sm' + }, + processing: { + container: 'dt-processing card' + }, + layout: { + row: 'row mt-2 justify-content-between', + cell: 'd-md-flex justify-content-between align-items-center', + tableCell: 'col-12', + start: 'dt-layout-start col-md-auto me-auto', + end: 'dt-layout-end col-md-auto ms-auto', + full: 'dt-layout-full col-md' + } + }); + /* Bootstrap paging button renderer */ + DataTable.ext.renderer.pagingButton.bootstrap = function (settings, buttonType, content, active, disabled) { + var btnClasses = ['dt-paging-button', 'page-item']; + if (active) { + btnClasses.push('active'); + } + if (disabled) { + btnClasses.push('disabled'); } + var li = DataTable.Dom.c('li').classAdd(btnClasses.join(' ')); + var a = DataTable.Dom + .c('button') + .classAdd('page-link') + .attr('role', 'link') + .attr('type', 'button') + .html(content) + .appendTo(li); + return { + display: li.get(0), + clicker: a.get(0) + }; + }; + DataTable.ext.renderer.pagingContainer.bootstrap = function (settings, buttonEls) { + return DataTable.Dom + .c('ul') + .classAdd('pagination') + .append(buttonEls) + .get(0); }; - - var hostEl = $(host); - // IE9 throws an 'unknown error' if document.activeElement is used - // inside an iframe or frame. - var activeEl; - - try { - // Because this approach is destroying and recreating the paging - // elements, focus is lost on the select button which is bad for - // accessibility. So we want to restore focus once the draw has - // completed - activeEl = hostEl.find(document.activeElement).data('dt-idx'); - } - catch (e) {} - - var paginationEl = hostEl.children('ul.pagination'); - - if (paginationEl.length) { - paginationEl.empty(); - } - else { - paginationEl = hostEl.html('
      ').children('ul').addClass('pagination'); - } - - attach( - paginationEl, - buttons - ); - - if ( activeEl !== undefined ) { - hostEl.find('[data-dt-idx='+activeEl+']').trigger('focus'); - } -}; -return DataTable; + return DataTable; })); diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/dataTables.buttons.js b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/dataTables.buttons.js index d4fd8dc7ba8..22001a331b3 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/dataTables.buttons.js +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/dataTables.buttons.js @@ -1,1232 +1,2008 @@ -/*! Buttons for DataTables 2.4.2 - * © SpryMedia Ltd - datatables.net/license +/*! Buttons 4.0.1 for DataTables + * Copyright (c) SpryMedia Ltd - datatables.net/license */ -(function( factory ){ - if ( typeof define === 'function' && define.amd ) { +(function(factory){ + if (typeof define === 'function' && define.amd) { // AMD - define( ['jquery', 'datatables.net'], function ( $ ) { - return factory( $, window, document ); - } ); + define(['datatables.net'], function (dt) { + return factory(window, document, dt); + }); } - else if ( typeof exports === 'object' ) { + else if (typeof exports === 'object') { // CommonJS - var jq = require('jquery'); - var cjsRequires = function (root, $) { - if ( ! $.fn.dataTable ) { - require('datatables.net')(root, $); + var cjsRequires = function (root) { + if (! root.DataTable) { + require('datatables.net')(root); } }; if (typeof window === 'undefined') { - module.exports = function (root, $) { - if ( ! root ) { + 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 ); + cjsRequires(root); + return factory(root, root.document, root.DataTable); }; } else { - cjsRequires( window, jq ); - module.exports = factory( jq, window, window.document ); + cjsRequires(window); + module.exports = factory(window, window.document, window.DataTable); } } 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; - -// Custom entity decoder for data export -var _entityDecoder = null; - -// 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); - } + factory(window, document, window.DataTable); } -} +}(function(window, document, DataTable) { + 'use strict'; -function _fadeOut(el, duration, fn) { - if ($.fn.animate) { - el.stop().fadeOut(duration, fn); - } - else { - el.css('display', 'none'); + var Dom = DataTable.Dom; + var util = DataTable.util; - if (fn) { - fn.call(el); + const collection = { + text: function (dt) { + return dt.i18n('buttons.collection', 'Collection'); + }, + className: 'buttons-collection', + closeButton: false, + dropIcon: true, + init: function (dt, button) { + button.attr('aria-expanded', false); + }, + action: function (e, dt, button, config) { + if (config._collection.isAttached()) { + 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') { + config._collection.find('a, button').eq(0).focus(); + } + }, + attr: { + 'aria-haspopup': 'dialog' } - } -} + // Also the popover options, defined in Buttons.popover + }; -/** - * [Buttons description] - * @param {[type]} - * @param {[type]} + /* + * Column visibility buttons for Buttons and DataTables. */ -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(); +// A collection of column visibility buttons + const colvis = function (dt, conf) { + var node = null; + var buttonConf = { + extend: 'collection', + init: function (dt, n) { + node = n; + }, + text: function (dt) { + return dt.i18n('buttons.colvis', 'Column visibility'); + }, + className: 'buttons-colvis', + closeButton: false, + buttons: [ + { + extend: 'columnsToggle', + columns: conf.columns, + columnText: conf.columnText + } + ] }; - } - - // 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++ + // Rebuild the collection with the new column structure if columns are reordered + dt.on('column-reorder.dt' + conf.namespace, function () { + dt.button(null, dt.button(null, node).node()).collectionRebuild([ + { + extend: 'columnsToggle', + columns: conf.columns, + columnText: conf.columnText + } + ]); + }); + return buttonConf; }; - - this.dom = { - container: $('<' + this.c.dom.container.tag + '/>').addClass(this.c.dom.container.className) +// Selected columns with individual buttons - toggle column visibility + const columnsToggle = function (dt, conf) { + var columns = dt + .columns(conf.columns) + .indexes() + .map(function (idx) { + return { + extend: 'columnToggle', + columns: idx, + columnText: conf.columnText + }; + }) + .toArray(); + return columns; }; - - 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; +// Single button to toggle column visibility + const columnToggle = function (dt, conf) { + return { + extend: 'columnVisibility', + columns: conf.columns, + columnText: conf.columnText + }; + }; +// Selected columns with individual buttons - set column visibility + const columnsVisibility = function (dt, conf) { + var columns = dt + .columns(conf.columns) + .indexes() + .map(function (idx) { + return { + extend: 'columnVisibility', + columns: idx, + visibility: conf.visibility, + columnText: conf.columnText + }; + }) + .toArray(); + return columns; + }; +// Single button to set column visibility + const columnVisibility = { + columns: undefined, // column selector + text: function (dt, button, conf) { + return conf._columnText(dt, conf); + }, + className: 'buttons-columnVisibility', + action: function (e, dt, button, conf) { + var col = dt.columns(conf.columns); + var curr = col.visible(); + col.visible(conf.visibility !== undefined ? conf.visibility : !(curr.length ? curr[0] : false)); + }, + init: function (dt, button, conf) { + var that = this; + var column = dt.column(conf.columns); + button.attr('data-cv-idx', conf.columns); + dt.on('column-visibility.dt' + conf.namespace, function (e, settings, index, state) { + if (column.index() === index && + !settings.destroying && + settings.table == dt.table().node()) { + that.active(state); + } + }).on('column-reorder.dt' + conf.namespace, function () { + // Button has been removed from the DOM + if (conf.destroying) { + return; + } + if (dt.columns(conf.columns).count() !== 1) { + return; + } + // Reassign the column for the updated indexes + column = dt.column(conf.columns); + // This button controls the same column index but the text for the column has + // changed + that.text(conf._columnText(dt, conf)); + // Since its a different column, we need to check its visibility + that.active(column.visible()); + }); + this.active(column.visible()); + }, + destroy: function (dt, button, conf) { + dt.off('column-visibility.dt' + conf.namespace).off('column-reorder.dt' + conf.namespace); + }, + _columnText: function (dt, conf) { + if (typeof conf.text === 'string') { + return conf.text; + } + var title = dt.column(conf.columns).title(); + var idx = dt.column(conf.columns).index(); + title = title + .replace(/\n/g, ' ') // remove new lines + .replace(//gi, ' ') // replace line breaks with spaces + .replace(//gi, ''); // remove select tags, including options text + // Strip HTML comments + title = DataTable.Buttons.stripHtmlComments(title); + // Use whatever HTML stripper DataTables is configured for + title = util.stripHtml(title).trim(); + return conf.columnText ? conf.columnText(dt, idx, title) : title; } - - button.conf.action = action; - - return this; - }, + }; + const colvisRestore = { + className: 'buttons-colvisRestore', + text: function (dt) { + return dt.i18n('buttons.colvisRestore', 'Restore visibility'); + }, + init: function (dt, button, conf) { + // Use a private parameter on the column. This gets moved around with the + // column if ColReorder changes the order + dt.columns().every(function () { + var init = this.init(); + if (init.__visOriginal === undefined) { + init.__visOriginal = this.visible(); + } + }); + }, + action: function (e, dt, button, conf) { + dt.columns().every(function (i) { + var init = this.init(); + this.visible(init.__visOriginal); + }); + } + }; + const colvisGroup = { + className: 'buttons-colvisGroup', + action: function (e, dt, button, conf) { + dt.columns(conf.show).visible(true, false); + dt.columns(conf.hide).visible(false, false); + dt.columns.adjust(); + }, + show: [], + hide: [] + }; /** - * 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 + * Get the newline character(s) + * + * @param config Button configuration + * @return Newline character */ - 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; - }, - + function newLine(config) { + return config.newline + ? config.newline + : navigator.userAgent.match(/Windows/) + ? '\r\n' + : '\n'; + } /** - * 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 + * Combine the data from the `buttons.exportData` method into a string that + * will be used in the export file. + * + * @param dt DataTables API instance + * @param config Button configuration + * @return The data to export */ - 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]; + function exportData(dt, config) { + var nl = newLine(config); + var data = dt.buttons.exportData(config.exportOptions); + var boundary = config.fieldBoundary; + var separator = config.fieldSeparator; + var reBoundary = new RegExp(boundary, 'g'); + var escapeChar = config.escapeChar !== undefined ? config.escapeChar : '\\'; + var join = function (a) { + var s = ''; + // If there is a field boundary, then we might need to escape it in + // the source data + for (var i = 0, ien = a.length; i < ien; i++) { + if (i > 0) { + s += separator; + } + s += boundary + ? boundary + + ('' + a[i]).replace(reBoundary, escapeChar + boundary) + + boundary + : a[i]; } - - buttons = base.buttons; - idx = split[split.length - 1] * 1; + return s; + }; + var header = ''; + var footer = ''; + var body = []; + if (config.header) { + header = + data.headerStructure + .map(function (row) { + return join(row.map(function (cell) { + return cell ? cell.title : ''; + })); + }) + .join(nl) + nl; } - - 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(); + if (config.footer && data.footer) { + footer = + data.footerStructure + .map(function (row) { + return join(row.map(function (cell) { + return cell ? cell.title : ''; + })); + }) + .join(nl) + nl; } + for (var i = 0, ien = data.body.length; i < ien; i++) { + body.push(join(data.body[i])); + } + return { + str: header + body.join(nl) + nl + footer, + rows: body.length + }; + } - 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); + /* + * Copy to clipboard button + */ + const copy = function (dt, conf) { + if (DataTable.ext.buttons.copyHtml5) { + return 'copyHtml5'; + } + }; + const copyHtml5 = { + className: 'buttons-copy buttons-html5', + text: function (dt) { + return dt.i18n('buttons.copy', 'Copy'); + }, + action: function (e, dt, button, config, cb) { + var exportD = exportData(dt, config); + var info = dt.buttons.exportInfo(config); + var newline = newLine(config); + var output = exportD.str; + var hiddenDiv = Dom.c('div').css({ + height: '1px', + width: '1px', + overflow: 'hidden', + position: 'fixed', + top: '0', + left: '0' + }); + if (info.title) { + output = info.title + newline + newline + output; } - - // 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 (info.messageTop) { + output = info.messageTop + newline + newline + output; } - - if (button.conf.postfixButtons) { - newButtons.push.apply(newButtons, button.conf.postfixButtons); + if (info.messageBottom) { + output = output + newline + newline + info.messageBottom; } - - 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 - ); + if (config.customize) { + output = config.customize(output, config, dt); } - } - - 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; + var textarea = Dom.c('textarea') + .prop('readonly', true) + .val(output) + .appendTo(hiddenDiv); + // For browsers that support the copy execCommand, try to use it + if (document.queryCommandSupported('copy')) { + hiddenDiv.appendTo(dt.table().container()); + textarea.get(0).focus(); + textarea.get(0).select(); + try { + var successful = document.execCommand('copy'); + hiddenDiv.remove(); + if (successful) { + if (config.copySuccess) { + dt.buttons.info(dt.i18n('buttons.copyTitle', 'Copy to clipboard'), dt.i18n('buttons.copySuccess', { + 1: 'Copied one row to clipboard', + _: 'Copied %d rows to clipboard' + }, exportD.rows), 2000); + } + cb(); + return; + } + } + catch (t) { + // noop + } + } + // Otherwise we show the text box and instruct the user to use it + var message = Dom.c('span') + .html(dt.i18n('buttons.copyKeys', 'Press ctrl or \u2318 + C to copy the table data
      to your system clipboard.

      ' + + 'To cancel, click this message or press escape.')) + .append(hiddenDiv); + dt.buttons.info(dt.i18n('buttons.copyTitle', 'Copy to clipboard'), message.get(0), 0); + // Select the text so when the user activates their system clipboard + // it will copy that text + textarea.get(0).focus(); + textarea.get(0).select(); + // Event to hide the message when the user is done + var container = message.closest('.dt-button-info'); + var close = function () { + container.off('click.buttons-copy'); + Dom.s(document).off('.buttons-copy'); + dt.buttons.info(false); + }; + container.on('click.buttons-copy', function () { + close(); + cb(); + }); + Dom.s(document) + .on('keydown.buttons-copy', function (e) { + if (e.keyCode === 27) { + // esc + close(); + cb(); + } + }) + .on('copy.buttons-copy cut.buttons-copy', function () { + close(); + cb(); + }); + }, + async: 100, + copySuccess: true, + exportOptions: {}, + fieldSeparator: '\t', + fieldBoundary: '', + header: true, + footer: true, + title: '*', + messageTop: '*', + messageBottom: '*' + }; - for (i = 0, ien = buttons.length; i < ien; i++) { - this.remove(buttons[i].node); + /* + * Save to CSV file + */ + const csv = function (dt, conf) { + if (typeof DataTable.ext.buttons.csvHtml5 === 'object' && + DataTable.ext.buttons.csvHtml5.available(dt, conf)) { + return 'csvHtml5'; } - - // 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; + }; + const csvHtml5 = { + bom: false, + className: 'buttons-csv buttons-html5', + available: function () { + return window.FileReader !== undefined && window.Blob ? true : false; + }, + text: function (dt) { + return dt.i18n('buttons.csv', 'CSV'); + }, + action: function (e, dt, button, config, cb) { + // Set the text + var output = exportData(dt, config).str; + var info = dt.buttons.exportInfo(config); + var charset = config.charset; + if (config.customize) { + output = config.customize(output, config, dt); } - } - - return this; - }, + if (charset !== false) { + if (!charset) { + charset = document.characterSet || document.charset; + } + if (charset) { + charset = ';charset=' + charset; + } + } + else { + charset = ''; + } + if (config.bom) { + output = String.fromCharCode(0xfeff) + output; + } + DataTable.fileSave(new Blob([output], { type: 'text/csv' + charset }), info.filename, true); + cb(); + }, + async: 100, + filename: '*', + extension: '.csv', + exportOptions: { + escapeExcelFormula: true + }, + fieldSeparator: ',', + fieldBoundary: '"', + escapeChar: '"', + charset: null, + header: true, + footer: true + }; + /* + * Save to Excel button + */ + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Local (private) functions + */ /** - * Enable / disable a button - * @param {node} node Button node - * @param {boolean} [flag=true] Enable / disable flag - * @return {Buttons} Self for chaining + * Get the sheet name for Excel exports. + * + * @param config Button configuration */ - enable: function (node, flag) { - if (flag === false) { - return this.disable(node); + var _sheetname = function (config) { + var sheetName = 'Sheet1'; + if (config.sheetName) { + sheetName = config.sheetName.replace(/[\[\]\*\/\\\?\:]/g, ''); } - - var button = this._nodeToButton(node); - $(button.node).removeClass(this.c.dom.button.disabled).prop('disabled', false); - - return this; - }, - + return sheetName; + }; /** - * Get a button's index + * Convert from numeric position to letter for column names in Excel * - * This is internally recursive - * @param {element} node Button to get the index of - * @return {string} Button index + * @param n Column number + * @return Column letter(s) name */ - 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; - } - } + function createCellPos(n) { + var ordA = 'A'.charCodeAt(0); + var ordZ = 'Z'.charCodeAt(0); + var len = ordZ - ordA + 1; + var s = ''; + while (n >= 0) { + s = String.fromCharCode((n % len) + ordA) + s; + n = Math.floor(n / len) - 1; } - - return null; - }, - + return s; + } + try { + var _serialiser = new XMLSerializer(); + } + catch (t) { + // noop + } /** - * Get the instance name for the button set selector - * @return {string} Instance name + * Recursively add XML files from an object's structure to a ZIP file. This + * allows the XSLX file to be easily defined with an object's structure matching + * the files structure. + * + * @param zip ZIP package + * @param obj Object to add (recursive) */ - name: function () { - return this.c.name; - }, - + function _addToZip(zip, obj) { + util.object.each(obj, (name, val) => { + if (util.is.plainObject(val)) { + var newDir = zip.folder(name); + _addToZip(newDir, val); + } + else { + var str = _serialiser.serializeToString(val); + // Safari, IE and Edge will put empty name space attributes onto + // various elements making them useless. This strips them out + str = str.replace(/<([^<>]*?) xmlns=""([^<>]*?)>/g, '<$1 $2>'); + zip.file(name, str); + } + }); + } /** - * 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 + * Create an XML node and add any children, attributes, etc without needing to + * be verbose in the DOM. + * + * @param doc XML document + * @param nodeName Node name + * @param opts Options - can be `attr` (attributes), `children` (child nodes) + * and `text` (text content) + * @return Created node */ - node: function (node) { - if (!node) { - return this.dom.container; + function _createNode(doc, nodeName, opts) { + var tempNode = doc.createElement(nodeName); + if (opts) { + if (opts.attr) { + Dom.s(tempNode).attr(opts.attr); + } + if (opts.children) { + util.object.each(opts.children, function (key, value) { + tempNode.appendChild(value); + }); + } + if (opts.text !== null && opts.text !== undefined) { + tempNode.appendChild(doc.createTextNode(opts.text)); + } } - - var button = this._nodeToButton(node); - return $(button.node); - }, - + return tempNode; + } /** - * 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. + * Get the width for an Excel column based on the contents of that column + * + * @param data Data for export + * @param col Column index + * @return Column width */ - processing: function (node, flag) { - var dt = this.s.dt; - var button = this._nodeToButton(node); - - if (flag === undefined) { - return $(button.node).hasClass('processing'); + function _excelColWidth(data, col) { + var max = data.header[col].length; + var len, lineSplit, str; + if (data.footer && data.footer[col] && data.footer[col].length > max) { + max = data.footer[col].length; } - - $(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); + for (var i = 0, ien = data.body.length; i < ien; i++) { + var point = data.body[i][col]; + str = point !== null && point !== undefined ? point.toString() : ''; + // If there is a newline character, workout the width of the column + // based on the longest line in the string + if (str.indexOf('\n') !== -1) { + lineSplit = str.split('\n'); + lineSplit.sort(function (a, b) { + return b.length - a.length; + }); + len = lineSplit[0].length; + } + else { + len = str.length; + } + if (len > max) { + max = len; + } + // Max width rather than having potentially massive column widths + if (max > 40) { + return 54; // 40 * 1.35 } } - - 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; + max *= 1.35; + // And a min width + return max > 6 ? max : 6; + } +// Excel - Pre-defined strings to build a basic XLSX file + var excelStrings = { + '_rels/.rels': '' + + '' + + '' + + '', + 'xl/_rels/workbook.xml.rels': '' + + '' + + '' + + '' + + '', + '[Content_Types].xml': '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + 'xl/workbook.xml': '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + 'xl/worksheets/sheet1.xml': '' + + '' + + '' + + '' + + '', + 'xl/styles.xml': '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + // Excel appears to use this as a dotted background regardless of values but + '' + // to be valid to the schema, use a patternFill + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + }; +// Note we could use 3 `for` loops for the styles, but when gzipped there is +// virtually no difference in size, since the above can be easily compressed +// Pattern matching for special number formats. Perhaps this should be exposed +// via an API in future? +// Ref: section 3.8.30 - built in formatters in open spreadsheet +// https://www.ecma-international.org/news/TC45_current_work/Office%20Open%20XML%20Part%204%20-%20Markup%20Language%20Reference.pdf + var _excelSpecials = [ + { + match: /^\-?\d+\.\d%$/, + style: 60, + fmt: function (d) { + return d / 100; } - - if (attachPoint !== undefined && attachPoint !== null) { - attachTo.splice(attachPoint, 0, built); - attachPoint++; + }, // Percent with d.p. + { + match: /^\-?\d+\.?\d*%$/, + style: 56, + fmt: function (d) { + return d / 100; } - else { - attachTo.push(built); + }, // Percent + { match: /^\-?\$[\d,]+.?\d*$/, style: 57 }, // Dollars + { match: /^\-?£[\d,]+.?\d*$/, style: 58 }, // Pounds + { match: /^\-?€[\d,]+.?\d*$/, style: 59 }, // Euros + { match: /^\-?\d+$/, style: 65 }, // Numbers without thousand separators + { match: /^\-?\d+\.\d{2}$/, style: 66 }, // Numbers 2 d.p. without thousands separators + { + match: /^\([\d,]+\)$/, + style: 61, + fmt: function (d) { + return -1 * d.replace(/[\(\)]/g, ''); } - - // 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 - ); + }, // Negative numbers indicated by brackets + { + match: /^\([\d,]+\.\d{2}\)$/, + style: 62, + fmt: function (d) { + return -1 * d.replace(/[\(\)]/g, ''); } - - // 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 - ); + }, // Negative numbers indicated by brackets - 2d.p. + { match: /^\-?[\d,]+$/, style: 63 }, // Numbers with thousand separators + { match: /^\-?[\d,]+\.\d{2}$/, style: 64 }, + { + match: /^(19\d\d|[2-9]\d\d\d)\-(0\d|1[012])\-[0123][\d]$/, + style: 67, + fmt: function (d) { + return Math.round(25569 + Date.parse(d) / (86400 * 1000)); } - - 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); + } //Date yyyy-mm-dd + ]; + var _excelMergeCells = function (rels, row, column, rowspan, colspan) { + var mergeCells = Dom.s(rels).find('mergeCells'); + mergeCells.get(0).appendChild(_createNode(rels, 'mergeCell', { + attr: { + ref: createCellPos(column) + + row + + ':' + + createCellPos(column + colspan - 1) + + (row + rowspan - 1) } + })); + mergeCells.attr('count', parseFloat(mergeCells.attr('count')) + 1); + }; + const excel = function (dt, conf) { + if (typeof DataTable.ext.buttons.excelHtml5 === 'object' && + DataTable.ext.buttons.excelHtml5.available(dt, conf)) { + return 'excelHtml5'; } - }, - - /** - * 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 + }; +// +// Excel (xlsx) export +// + const excelHtml5 = { + className: 'buttons-excel buttons-html5', + available: function () { + return window.FileReader !== undefined && + DataTable.Buttons.jszip() !== undefined && + _serialiser + ? true + : false; + }, + text: function (dt) { + return dt.i18n('buttons.excel', 'Excel'); + }, + action: function (e, dt, button, config, cb) { + var rowPos = 0; + var dataStartRow, dataEndRow; + var getXml = function (type) { + var str = excelStrings[type]; + return new window.DOMParser().parseFromString(str, 'text/xml'); }; - } - - // 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 rels = getXml('xl/worksheets/sheet1.xml'); + var relsGet = rels.getElementsByTagName('sheetData')[0]; + var xlsx = { + _rels: { + '.rels': getXml('_rels/.rels') + }, + xl: { + _rels: { + 'workbook.xml.rels': getXml('xl/_rels/workbook.xml.rels') + }, + 'workbook.xml': getXml('xl/workbook.xml'), + 'styles.xml': getXml('xl/styles.xml'), + worksheets: { + 'sheet1.xml': rels + } + }, + '[Content_Types].xml': getXml('[Content_Types].xml') }; - - 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); + var data = dt.buttons.exportData(config.exportOptions); + var columnTypes = dt.columns(config.exportOptions.columns).types(); + var currentRow, rowNode; + var addRow = function (row) { + currentRow = rowPos + 1; + rowNode = _createNode(rels, 'row', { attr: { r: currentRow } }); + for (var i = 0, ien = row.length; i < ien; i++) { + // Concat both the Cell Columns as a letter and the Row of the + // cell. + var cellId = createCellPos(i) + '' + currentRow; + var cell = null; + // For null, undefined of blank cell, continue so it doesn't + // create the _createNode + if (row[i] === null || row[i] === undefined || row[i] === '') { + if (config.createEmptyCells === true) { + row[i] = ''; + } + else { + continue; + } } - - if (clickBlurs) { - button.trigger('blur'); + var originalContent = row[i]; + row[i] = + typeof row[i].trim === 'function' ? row[i].trim() : row[i]; + // Special number formatting options + for (var j = 0, jen = _excelSpecials.length; j < jen; j++) { + var special = _excelSpecials[j]; + // TODO Need to provide the ability for the specials to say + // if they are returning a string, since at the moment it is + // assumed to be a number + if (row[i].match && + !row[i].match(/^0\d+/) && + row[i].match(special.match)) { + var val = row[i].replace(/[^\d\.\-]/g, ''); + if (special.fmt) { + val = special.fmt(val); + } + cell = _createNode(rels, 'c', { + attr: { + r: cellId, + s: special.style + }, + children: [_createNode(rels, 'v', { text: val })] + }); + break; + } } - }) - .on('keypress.dtb', function (e) { - if (e.keyCode === 13) { - e.preventDefault(); - - if (!button.hasClass(dom.disabled) && config.action) { - action(e, dt, button, config); + if (!cell) { + if (columnTypes[i].includes('num') && + (typeof row[i] === 'number' || + (row[i].match && + row[i].match(/^-?\d+(\.\d+)?([eE]\-?\d+)?$/) && // Includes exponential format + !row[i].match(/^0\d+/)))) { + // Detect numbers - don't match numbers with leading zeros + // or a negative anywhere but the start + cell = _createNode(rels, 'c', { + attr: { + t: 'n', + r: cellId + }, + children: [_createNode(rels, 'v', { text: row[i] })] + }); + } + else { + // String output - replace non standard characters for text output + /*eslint no-control-regex: "off"*/ + var text = !originalContent.replace + ? originalContent + : originalContent.replace(/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, ''); + cell = _createNode(rels, 'c', { + attr: { + t: 'inlineStr', + r: cellId + }, + children: { + row: _createNode(rels, 'is', { + children: { + row: _createNode(rels, 't', { + text: text, + attr: { + 'xml:space': 'preserve' + } + }) + } + }) + } + }); } } + rowNode.appendChild(cell); + } + relsGet.appendChild(rowNode); + rowPos++; + }; + var addHeader = function (structure) { + structure.forEach(function (row) { + addRow(row.map(function (cell) { + return cell ? cell.title : ''; + })); + Dom.s(rels).find('row').last().find('c').attr('s', '2'); // bold + // Add any merge cells + row.forEach(function (cell, columnCounter) { + if (cell && (cell.colspan > 1 || cell.rowspan > 1)) { + _excelMergeCells(rels, rowPos, columnCounter, cell.rowspan, cell.colspan); + } + }); }); - - // Make `a` tags act like a link - if (tag.toLowerCase() === 'a') { - button.attr('href', '#'); + }; + // Title and top messages + var exportInfo = dt.buttons.exportInfo(config); + if (exportInfo.title) { + addRow([exportInfo.title]); + _excelMergeCells(rels, rowPos, 0, 1, data.header.length); + Dom.s(rels).find('row').last().find('c').attr('s', '51'); // centre } - - // Button tags should have `type=button` so they don't have any default behaviour - if (tag.toLowerCase() === 'button') { - button.attr('type', 'button'); + if (exportInfo.messageTop) { + addRow([exportInfo.messageTop]); + _excelMergeCells(rels, rowPos, 0, 1, data.header.length); } - - 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; + // Table header + if (config.header) { + addHeader(data.headerStructure); } - else { - button.html(text(config.text)); - textNode = button; + dataStartRow = rowPos; + // Table body + for (var n = 0, ie = data.body.length; n < ie; n++) { + addRow(data.body[n]); } - - if (config.enabled === false) { - button.addClass(dom.disabled); + dataEndRow = rowPos; + // Table footer + if (config.footer && data.footer) { + addHeader(data.footerStructure); } - - if (config.className) { - button.addClass(config.className); + // Below the table + if (exportInfo.messageBottom) { + addRow([exportInfo.messageBottom]); + _excelMergeCells(rels, rowPos, 0, 1, data.header.length); } - - if (config.titleAttr) { - button.attr('title', text(config.titleAttr)); + // Set column widths + var cols = _createNode(rels, 'cols'); + Dom.s(rels).find('worksheet').prepend(cols); + for (var i = 0, ien = data.header.length; i < ien; i++) { + cols.appendChild(_createNode(rels, 'col', { + attr: { + min: i + 1, + max: i + 1, + width: _excelColWidth(data, i), + customWidth: 1 + } + })); } - - if (config.attr) { - button.attr(config.attr); + // Workbook modifications + var workbook = xlsx.xl['workbook.xml']; + Dom.s(workbook).find('sheets sheet').attr('name', _sheetname(config)); + // Auto filter for columns + if (config.autoFilter) { + let node = _createNode(rels, 'autoFilter', { + attr: { + ref: 'A' + + dataStartRow + + ':' + + createCellPos(data.header.length - 1) + + dataEndRow + } + }); + Dom.s(node).insertBefore(Dom.s(rels).find('mergeCells')); + Dom.s(workbook) + .find('definedNames') + .append(_createNode(workbook, 'definedName', { + attr: { + name: '_xlnm._FilterDatabase', + localSheetId: '0', + hidden: 1 + }, + text: "'" + + _sheetname(config).replace(/'/g, "''") + + "'!$A$" + + dataStartRow + + ':$' + + createCellPos(data.header.length - 1) + + '$' + + dataEndRow + })); } - - if (!config.namespace) { - config.namespace = '.dt-button-' + _buttonCounter++; + // Let the developer customise the document if they want to + if (config.customize) { + config.customize(xlsx, config, dt); } - - if (config.config !== undefined && config.config.split) { - config.split = config.config.split; + // Excel doesn't like an empty mergeCells tag + let merge = Dom.s(rels).find('mergeCells'); + if (merge.children().count() === 0) { + merge.remove(); } - } - 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 jszip = DataTable.Buttons.jszip(); + var zip = new jszip(); + var zipConfig = { + compression: 'DEFLATE', + type: 'blob', + mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }; - - 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); - } - } + _addToZip(zip, xlsx); + // Modern Excel has a 218 character limit on the file name + path of the + // file (why!?) + // https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3 + // So we truncate to allow for this. + var filename = exportInfo.filename; + if (filename.length > 175) { + filename = filename.substr(0, 175); + } + // Let the developer customize the final zip file if they want to before + // it is generated and sent to the browser + if (config.customizeZip) { + config.customizeZip(zip, data, filename); + } + if (zip.generateAsync) { + // JSZip 3+ + zip.generateAsync(zipConfig).then(function (blob) { + DataTable.fileSave(blob, filename); + cb(); }); - - if (config.split.length === 0) { - dropButton.addClass('dtb-hide-drop'); } + else { + // JSZip 2.5 + DataTable.fileSave(zip.generate(zipConfig), filename); + cb(); + } + }, + async: 100, + filename: '*', + extension: '.xlsx', + exportOptions: {}, + header: true, + footer: true, + title: '*', + messageTop: '*', + messageBottom: '*', + createEmptyCells: false, + autoFilter: false, + sheetName: '' + }; - splitDiv.append(dropButton).attr(dropButtonConfig.attr); + const colOrder = function (dt, conf) { + var columns = dt.columns(conf.columns).indexes().toArray(); + return { + extend: 'collection', + text: dt.i18n('buttons.order.dropdown', 'Order'), + className: 'buttons-order', + autoClose: true, + buttons: columns.map(idx => ({ + extend: 'columnOrder', + column: idx + })) + }; + }; + const columnOrder = function (dt, conf) { + let column = dt.column(conf.column); + let columnIdx = column.index(); + let orderSequence = dt.settings()[0].columns[columnIdx].orderSequence; + let split = undefined; + // Split buttons are only relevant if you can order both ways + if (orderSequence.includes('asc') && orderSequence.includes('desc')) { + split = [ + { + text: column.title() + + ' ' + + dt.i18n('buttons.order.asc', 'ascending'), + action: () => { + column.order('asc').draw(); + }, + init: function () { + dt.on('order', () => { + this.active(orderActive(dt, columnIdx, 'asc')); + }); + } + }, + { + text: column.title() + + ' ' + + dt.i18n('buttons.order.desc', 'descending'), + action: () => { + column.order('desc').draw(); + }, + init: function () { + dt.on('order', () => { + this.active(orderActive(dt, columnIdx, 'desc')); + }); + } + } + ]; } - 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 + text: column.title(), + className: 'buttons-column-order', + action: function (e, dt, node, config, cb) { + // Toggle between the first and second options in the order + // sequence + if (orderActive(dt, columnIdx, orderSequence[0]) && + orderSequence.length > 1) { + column.order(orderSequence[1]).draw(); + } + else { + column.order(orderSequence[0]).draw(); + } + }, + init: function (dt, node, config) { + dt.on('order', () => { + this.active(orderActive(dt, columnIdx)); + }); + }, + split: split }; - }, - + }; /** - * 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 + * Determine if a column ordering is active + * + * @param dt Table in question + * @param colIdx Column index + * @param dir Direction. If given, direction is considered, if not any active + * ordering on the column is enough. + * @returns True if the column is being ordered on, false otherwise */ - _nodeToButton: function (node, buttons) { - if (!buttons) { - buttons = this.s.buttons; + function orderActive(dt, colIdx, dir) { + let applied = dt.order(); + let orderIdx = applied.findIndex(prop => { + return prop[0] === colIdx; + }); + if (orderIdx === -1) { + return false; } - - 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; - } - } + else if (dir) { + return applied[orderIdx][1] === dir ? true : false; } - }, + return true; + } - /** - * 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; + /* + * Page length + */ + const pageLength = function (dt) { + var lengthMenu = dt.settings()[0].lengthMenu; + 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]; } - - 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; + else { + for (var i = 0; i < lengthMenu.length; i++) { + var option = lengthMenu[i]; + // Support for DataTables 2 object in the array + if (util.is.plainObject(option)) { + vals.push(option.value); + lang.push(option.label); + } + else { + vals.push(option); + lang.push(option); } } } - }, - - /** - * 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; + return { + extend: 'collection', + text: text, + className: 'buttons-page-length', + autoClose: true, + buttons: vals.map(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); } + }; + }; - if (conf.key === character) { - e._buttonsHandled = true; - $(node).click(); + /* + * Save to PDF button - using pdfMake - http://pdfmake.org + */ + const pdf = function (dt, conf) { + if (typeof DataTable.ext.buttons.pdfHtml5 === 'object' && + DataTable.ext.buttons.pdfHtml5.available(dt, conf)) { + return 'pdfHtml5'; + } + }; + const pdfHtml5 = { + className: 'buttons-pdf buttons-html5', + available: function () { + return window.FileReader !== undefined && DataTable.Buttons.pdfMake(); + }, + text: function (dt) { + return dt.i18n('buttons.pdf', 'PDF'); + }, + action: function (e, dt, button, config, cb) { + var data = dt.buttons.exportData(config.exportOptions); + var info = dt.buttons.exportInfo(config); // Need better typing + var rows = []; + if (config.header) { + data.headerStructure.forEach(function (row) { + rows.push(row.map(function (cell) { + return cell + ? { + text: cell.title, + colSpan: cell.colspan, + rowSpan: cell.rowspan, + style: 'tableHeader' + } + : {}; + })); + }); } - 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(); + for (var i = 0, ien = data.body.length; i < ien; i++) { + rows.push(data.body[i].map(function (d) { + return { + text: d === null || d === undefined + ? '' + : typeof d === 'string' + ? d + : d.toString() + }; + })); } - }; - - 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); + if (config.footer) { + data.footerStructure.forEach(function (row) { + rows.push(row.map(function (cell) { + return cell + ? { + text: cell.title, + colSpan: cell.colspan, + rowSpan: cell.rowspan, + style: 'tableFooter' + } + : {}; + })); + }); + } + var doc = { + pageSize: config.pageSize, + pageOrientation: config.orientation, + content: [ + { + style: 'table', + table: { + headerRows: config.header + ? data.headerStructure.length + : 0, + footerRows: config.footer // Used for styling, doesn't do anything in pdfmake + ? data.footerStructure.length + : 0, + body: rows + }, + layout: { + hLineWidth: function (i, node) { + if (i === 0 || i === node.table.body.length) { + return 0; + } + return 0.5; + }, + vLineWidth: function () { + return 0; + }, + hLineColor: function (i, node) { + return i === node.table.headerRows || + i === + node.table.body.length - + node.table.footerRows + ? '#333' + : '#ddd'; + }, + fillColor: function (rowIndex) { + if (rowIndex < data.headerStructure.length) { + return '#fff'; + } + return rowIndex % 2 === 0 ? '#f3f3f3' : null; + }, + paddingTop: function () { + return 5; + }, + paddingBottom: function () { + return 5; + } + } + } + ], + styles: { + tableHeader: { + bold: true, + fontSize: 11, + alignment: 'center' + }, + tableFooter: { + bold: true, + fontSize: 11, + alignment: 'center' + }, + table: { + margin: [0, 5, 0, 5] + }, + title: { + alignment: 'center', + fontSize: 13 + }, + message: {} + }, + defaultStyle: { + fontSize: 10 } + }; + if (info.messageTop) { + doc.content.unshift({ + text: info.messageTop, + style: 'message', + margin: [0, 0, 0, 12] + }); } - }; - - 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(''); - } - }, + if (info.messageBottom) { + doc.content.push({ + text: info.messageBottom, + style: 'message', + margin: [0, 0, 0, 12] + }); + } + if (info.title) { + doc.content.unshift({ + text: info.title, + style: 'title', + margin: [0, 0, 0, 12] + }); + } + if (config.customize) { + config.customize(doc, config, dt); + } + var pdf = DataTable.Buttons.pdfMake().createPdf(doc); + if (config.download === 'open') { + pdf.open(); + } + else { + pdf.download(info.filename); + } + cb(); + }, + async: 100, + title: '*', + filename: '*', + extension: '.pdf', + exportOptions: {}, + orientation: 'portrait', + // This isn't perfect, but it is close + pageSize: navigator.language === 'en-US' || navigator.language === 'en-CA' + ? 'LETTER' + : 'A4', + header: true, + footer: true, + messageTop: '*', + messageBottom: '*', + customize: null, + download: 'download' + }; + /* + * Print button for Buttons and DataTables. + */ + var _link = document.createElement('a'); /** - * Resolve a button configuration - * @param {string|function|object} conf Button config to resolve - * @return {object} Button configuration - * @private + * Convert a URL from a relative to an absolute address so it will work + * correctly in the popup window which has no base URL. + * + * @param href URL */ - _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'; + var _relToAbs = function (href) { + // Assign to a link on the original page so the browser will do all the + // hard work of figuring out where the file actually is + _link.href = href; + var linkHost = _link.host; + // IE doesn't have a trailing slash on the host + // Chrome has it on the pathname + if (linkHost.indexOf('/') === -1 && _link.pathname.indexOf('/') !== 0) { + linkHost += '/'; + } + return _link.protocol + '//' + linkHost + _link.pathname + _link.search; + }; + const print = { + className: 'buttons-print', + text: function (dt) { + return dt.i18n('buttons.print', 'Print'); + }, + action: function (e, dt, button, config, cb) { + // Make sure that decodeEntities are set for XSS protection + var data = dt.buttons.exportData(util.object.assign({ decodeEntities: false }, config.exportOptions)); + // Could do with better typing for the config object - it extends + // ButtonsApiExportInfoParameter + var exportInfo = dt.buttons.exportInfo(config); + // Get the classes for the columns from the header cells + var columnClasses = dt + .columns(config.exportOptions.columns) + .nodes() + .map(function (n) { + return n.className; + }) + .toArray(); + var addRow = function (d, tag) { + var str = ''; + for (var i = 0, ien = d.length; i < ien; i++) { + // null and undefined aren't useful in the print output + var dataOut = d[i] === null || d[i] === undefined ? '' : d[i]; + var classAttr = columnClasses[i] + ? 'class="' + columnClasses[i] + '"' + : ''; + str += + '<' + + tag + + ' ' + + classAttr + + '>' + + dataOut + + ''; } + return str + ''; + }; + // Construct a table for printing + var html = ''; + if (config.header) { + var headerRows = data.headerStructure.map(function (row) { + return ('' + + row + .map(function (cell) { + return cell + ? '' + : ''; + }) + .join('') + + ''); + }); + html += '' + headerRows.join('') + ''; } - - 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; + html += ''; + for (var i = 0, ien = data.body.length; i < ien; i++) { + html += addRow(data.body[i], 'td'); } - - 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; + html += ''; + if (config.footer && data.footer) { + var footerRows = data.footerStructure.map(function (row) { + return ('' + + row + .map(function (cell) { + return cell + ? '' + : ''; + }) + .join('') + + ''); + }); + html += '' + footerRows.join('') + ''; } - - // Stash the current class name - var originalClassName = objArray.className; - - if (conf.config !== undefined && objArray.config !== undefined) { - conf.config = $.extend({}, objArray.config, conf.config); + html += '
      ' + + cell.title + + '
      ' + + cell.title + + '
      '; + // Open a new window for the printable table + var win = window.open('', ''); + if (!win) { + dt.buttons.info(dt.i18n('buttons.printErrorTitle', 'Unable to open print view'), dt.i18n('buttons.printErrorMsg', 'Please allow popups in your browser for this site to be able to view the print view.'), 5000); + return; } - - 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; + win.document.close(); + // Inject the title and also a copy of the style and link tags from this + // document so the table can retain its base styling. This avoids + // issues with Content Security Policy (CSP) and is compatible with modern browsers. + win.document.title = exportInfo.title; + Dom.s('style, link[rel="stylesheet"]').each(function (el) { + let node = el.cloneNode(true); + if (node.tagName.toLowerCase() === 'link') { + node.href = _relToAbs(node.href); + } + win.document.head.appendChild(node); + }); + // Add any custom scripts (for example for paged.js) + if (config.customScripts) { + config.customScripts.forEach(function (script) { + var tag = win.document.createElement('script'); + tag.src = script; + win.document.getElementsByTagName('head')[0].appendChild(tag); + }); + } + // Inject the table and other surrounding information + win.document.body.innerHTML = + '

      ' + + exportInfo.title + + '

      ' + + '
      ' + + (exportInfo.messageTop || '') + + '
      ' + + html + + '
      ' + + (exportInfo.messageBottom || '') + + '
      '; + Dom.s(win.document.body).classAdd('dt-print-view'); + Dom.s(win.document.body) + .find('img') + .each(function (img) { + img.setAttribute('src', _relToAbs(img.getAttribute('src') || '')); + }); + if (config.customize) { + config.customize(win, config, dt); } + // Allow stylesheets time to load + var autoPrint = function () { + if (config.autoPrint) { + win.print(); // blocking - so close will not + win.close(); // execute until this is done + } + }; + win.setTimeout(autoPrint, 1000); + cb(); + }, + async: 100, + customScripts: null, + title: '*', + messageTop: '*', + messageBottom: '*', + exportOptions: {}, + header: true, + footer: true, + autoPrint: true, + customize: null + }; - // 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; + const spacer = { + style: 'empty', + spacer: true, + text: function (dt) { + return dt.i18n('buttons.spacer', ''); } + }; - // 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]); - } + const split = { + text: function (dt) { + return dt.i18n('buttons.split', 'Split'); + }, + className: 'buttons-split', + closeButton: false, + init: function (dt, button) { + 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 + }; - var prefixButtons = conf.prefixButtons; - if (prefixButtons) { - if (!conf.buttons) { - conf.buttons = []; - } + Object.assign(DataTable.ext.buttons, { + colOrder, + columnOrder, + colvis, + columnsToggle, + columnToggle, + columnsVisibility, + columnVisibility, + colvisRestore, + colvisGroup, + copy, + copyHtml5, + csv, + csvHtml5, + excel, + excelHtml5, + pdf, + pdfHtml5, + pageLength, + print, + spacer, + collection, + split, + }); - for (i = 0, ien = prefixButtons.length; i < ien; i++) { - conf.buttons.splice(i, 0, prefixButtons[i]); - } + /* +* FileSaver.js +* A saveAs() FileSaver implementation. +* +* By Eli Grey, http://eligrey.com +* +* License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT) +* source : http://purl.eligrey.com/github/FileSaver.js +*/ + var _global = window; + function isUtf8TextOrXml(typeString) { + if (!typeString) { + return false; + } + const [mediaType, ...params] = typeString.toLowerCase().split(';').map(s => s.trim()); + const isTargetType = mediaType.startsWith('text/') || + mediaType === 'application/xml' || + (mediaType.includes('/') && mediaType.endsWith('+xml')); + if (!isTargetType) { + return false; + } + return params.some(param => { + const [key, val] = param.split('=').map(s => s.trim()); + return key === 'charset' && val === 'utf-8'; + }); + } + function bom(blob, opts) { + if (typeof opts === 'undefined') + opts = { autoBom: false }; + else if (typeof opts !== 'object') { + console.warn('Deprecated: Expected third argument to be a object'); + opts = { autoBom: !opts }; + } + // prepend BOM for UTF-8 XML and text/* types (including HTML) + // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF + if (opts.autoBom && isUtf8TextOrXml(blob.type)) { + return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type }); + } + return blob; + } + function download(url, name, opts) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.responseType = 'blob'; + xhr.onload = function () { + saveAs(xhr.response, name, opts); + }; + xhr.onerror = function () { + console.error('could not download file'); + }; + xhr.send(); + } + function corsEnabled(url) { + var xhr = new XMLHttpRequest(); + // use sync to avoid popup blocker + xhr.open('HEAD', url, false); + try { + xhr.send(); + } + catch (e) { } + return xhr.status >= 200 && xhr.status <= 299; + } +// `a.click()` doesn't work for all browsers (#465) + function click(node) { + try { + node.dispatchEvent(new MouseEvent('click')); } + catch (e) { + var evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); + node.dispatchEvent(evt); + } + } +// Detect WebView inside a native macOS app by ruling out all browsers +// We just need to check for 'Safari' because all other browsers (besides Firefox) include that too +// https://www.whatismybrowser.com/guides/the-latest-user-agent/macos + var isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent); + var saveAs = _global.saveAs || ( +// probably in some web worker + (typeof window !== 'object' || window !== _global) + ? function saveAs() { } + // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView + : ('download' in HTMLAnchorElement.prototype && !isMacOSWebView) + ? function saveAs(blob, name, opts) { + var URL = _global.URL || _global.webkitURL; + // Namespace is used to prevent conflict w/ Chrome Poper Blocker extension (Issue #561) + var a = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); + name = name || blob.name || 'download'; + a.download = name; + a.rel = 'noopener'; // tabnabbing + // TODO: detect chrome extensions & packaged apps + // a.target = '_blank' + if (typeof blob === 'string') { + // Support regular links + a.href = blob; + if (a.origin !== location.origin) { + corsEnabled(a.href) + ? download(blob, name, opts) + : click(a, a.target = '_blank'); + } + else { + click(a); + } + } + else { + // Support blobs + a.href = URL.createObjectURL(blob); + setTimeout(function () { URL.revokeObjectURL(a.href); }, 4E4); // 40s + setTimeout(function () { click(a); }, 0); + } + } + // Use msSaveOrOpenBlob as a second approach + : 'msSaveOrOpenBlob' in navigator + ? function saveAs(blob, name, opts) { + name = name || blob.name || 'download'; + if (typeof blob === 'string') { + if (corsEnabled(blob)) { + download(blob, name, opts); + } + else { + var a = document.createElement('a'); + a.href = blob; + a.target = '_blank'; + setTimeout(function () { click(a); }); + } + } + else { + navigator.msSaveOrOpenBlob(bom(blob, opts), name); + } + } + // Fallback to using FileReader and a popup + : function saveAs(blob, name, opts, popup) { + // Open a popup immediately do go around popup blocker + // Mostly only available on user interaction and the fileReader is async so... + popup = popup || open('', '_blank'); + if (popup) { + popup.document.title = + popup.document.body.innerText = 'downloading...'; + } + if (typeof blob === 'string') + return download(blob, name, opts); + var force = blob.type === 'application/octet-stream'; + var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari; + var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent); + if ((isChromeIOS || (force && isSafari) || isMacOSWebView) && typeof FileReader !== 'undefined') { + // Safari doesn't allow downloading of blob URLs + var reader = new FileReader(); + reader.onloadend = function () { + var url = reader.result; + url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;'); + if (popup) + popup.location.href = url; + else + location = url; + popup = null; // reverse-tabnabbing #460 + }; + reader.readAsDataURL(blob); + } + else { + var URL = _global.URL || _global.webkitURL; + var url = URL.createObjectURL(blob); + if (popup) + popup.location = url; + else + location.href = url; + popup = null; // reverse-tabnabbing #460 + setTimeout(function () { URL.revokeObjectURL(url); }, 4E4); // 40s + } + }); - 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( - { + if (!DataTable.versionCheck('3')) { + throw 'Warning: Buttons requires DataTables 3 or newer'; + } +// Expose file saver on the DataTables API. + DataTable.fileSave = saveAs; + const _exportTextarea = document.createElement('textarea'); +// 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; +// Custom entity decoder for data export + var _entityDecoder = null; + var useJszip; + var usePdfmake; + function _jsZip() { + return useJszip || window.JSZip; + } + function _pdfMake() { + return usePdfmake || window.pdfMake; + } + function fadeIn(el, duration = 400, fn) { + el.css({ + opacity: '0' + }).transition({ opacity: '1' }, duration, null, fn); + } + function fadeOut(el, duration = 400, fn) { + el.css({ + opacity: '1' + }).transition({ opacity: '0' }, duration, null, fn); + } + class Buttons { + /** + * Set the pdfMake library for use with the pdfHtml5 button type + * + * @param _ + * @returns pdfMake library that is set + */ + static pdfMake(_) { + if (_) { + usePdfmake = _; + } + return _pdfMake(); + } + /** + * Set the JSZip library for use with the excelHtml5 button type + * + * @param _ + * @returns JSZip library that is set + */ + static jszip(_) { + if (_) { + useJszip = _; + } + return _jsZip(); + } + /** + * Display (and replace if there is an existing one) a popover attached to a + * button + * + * @param contentIn Content to show + * @param hostButton DT API instance of the button + * @param inOpts Options (see object below for all options) + */ + popover(contentIn, hostButton, inOpts) { + var dt = hostButton; + var c = this.c; + var closed = false; + var options = util.object.assign({ align: 'button-left', // button-right, dt-container, split-left, split-right autoClose: false, background: true, @@ -1241,897 +2017,1741 @@ $.extend(Buttons.prototype, { 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; + }, inOpts); + var containerSelector = options.tag + '.' + options.containerClassName.replace(/ /g, '.'); + var hostButtonNode = hostButton.node(); + var hostNode = options.collectionLayout.includes('fixed') + ? Dom.s('body') + : hostButton.node(); + var close = function () { + closed = true; + fadeOut(Dom.s(containerSelector), options.fade, function () { + this.detach(); + }); + dt.buttons('[aria-haspopup="dialog"][aria-expanded="true"]') + .nodes() + .attr('aria-expanded', 'false'); + Dom.s('div.dt-button-background').off('click.dtb-collection'); + Buttons.background(false, options.backgroundClassName, options.fade, hostNode.get(0)); + Dom.w.off('resize.resize.dtb-collection'); + Dom.s('body').off('.dtb-collection'); + dt.off('buttons-action.b-internal'); + dt.off('destroy.dtb-popover'); + Dom.s('body').trigger('buttons-popover-hide.dt'); + }; + if (contentIn === false) { + close(); + return; } - - if (options.align === 'button-right' || display.hasClass(options.rightAlignClassName)) { - left = buttonPosition.left - popoverSizes.width + hostNode.outerWidth(); + var existingExpanded = dt + .buttons('[aria-haspopup="dialog"][aria-expanded="true"]') + .nodes(); + if (existingExpanded.count()) { + // Reuse the current position if the button that was triggered is + // inside an existing collection + if (hostNode.closest(containerSelector).count()) { + hostNode = existingExpanded.eq(0); + } + close(); } - - // 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; - } + let content = typeof contentIn === 'string' + ? Dom.c('div').html(contentIn).children() + : Dom.s(contentIn); + // Sort buttons if defined + if (options.sort) { + var elements = content.find('button').mapTo(function (el) { + return { + text: Dom.s(el).text(), + el: el + }; + }); + elements.sort(function (a, b) { + return a.text.localeCompare(b.text); + }); + content.append(elements.map(function (v) { + return v.el; + })); } - - // Window adjustment - if (containerPosition.left + left + popoverSizes.width > $(window).width()) { - // Overflowing the document to the right - left = $(window).width() - popoverSizes.width - containerPosition.left; + // Try to be smart about the layout + var cnt = content.find('.dt-button').count(); + var mod = ''; + if (cnt === 3) { + mod = 'dtb-b3'; } - - if (buttonOffset.left + left < 0) { - // Off to the left of the document - left = -buttonOffset.left; + else if (cnt === 2) { + mod = 'dtb-b2'; } - - 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; + else if (cnt === 1) { + mod = 'dtb-b1'; } - - if (containerPosition.top + top < $(window).scrollTop()) { - // Correction for when the top is beyond the top of the page - top = buttonPosition.top + hostNode.outerHeight(); + var display = Dom.c(options.tag) + .classAdd(options.containerClassName) + .classAdd(options.collectionLayout) + .classAdd(options.splitAlignClass) + .classAdd(mod) + .css('opacity', '0') + .attr({ + 'aria-modal': true, + role: 'dialog' + }); + content + .classAdd(options.contentClassName) + .attr('role', 'menu') + .appendTo(display); + hostButtonNode.attr('aria-expanded', 'true'); + if (!hostNode.isAttached()) { + let possibilities = Dom.s(document.body).children('div, section, p'); + hostNode = possibilities.eq(possibilities.count() - 1); } - - // 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; + if (options.popoverTitle) { + display.prepend('
      ' + + options.popoverTitle + + '
      '); + } + else if (options.collectionTitle) { + display.prepend('
      ' + + options.collectionTitle + + '
      '); + } + if (options.closeButton) { + display + .prepend('
      ×
      ') + .classAdd('dtb-collection-closeable'); + } + if (hostNode.get(0) === document.body) { + display.appendTo(document.body); + } + else { + display.insertAfter(hostNode); + } + var tableContainer = Dom.s(hostButton.table().container()); + var position = display.css('position'); + if (options.span === 'container' || options.align === 'dt-container') { + hostNode = hostNode.parent(); + display.css('width', tableContainer.width() + 'px'); + } + // 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 = Dom.s(hostNode.get(0).offsetParent); + var buttonPosition = hostNode.position(); + var buttonOffset = hostNode.offset(); + var containerPosition = offsetParent.position(); + // Set the initial position so we can read height / width + var top = buttonPosition.top + hostNode.height('outer'); + var left = buttonPosition.left; + display.css({ + top: top + 'px', + left: left + 'px' + }); + // Get the popover position + var computed = window.getComputedStyle(display.get(0)); + var displayOffset = display.offset(); + var displayWidth = display.width('outer'); + var displayHeight = display.height('outer'); + var popoverSizes = { + height: displayHeight, + width: displayWidth, + right: displayOffset.left + displayWidth, + bottom: displayOffset.top + displayHeight, + marginTop: parseFloat(computed.marginTop), + marginBottom: parseFloat(computed.marginBottom), + top: displayOffset.top, + left: displayOffset.left + }; + // First position per the class requirements - pop up and right align + if (options.dropup) { + top = + buttonPosition.top - + popoverSizes.height - + popoverSizes.marginTop - + popoverSizes.marginBottom; } - - 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; + if (options.align === 'button-right' || + display.classHas(options.rightAlignClassName)) { + left = + buttonPosition.left - + popoverSizes.width + + hostNode.width('outer'); + } + // 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; } - close(); + } + // Window adjustment + if (containerPosition.left + left + popoverSizes.width > + Dom.w.width()) { + // Overflowing the document to the right + left = + Dom.w.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 > + Dom.w.height() + Dom.w.scrollTop()) { + // Pop up if otherwise we'd need the user to scroll down + top = + buttonPosition.top - + popoverSizes.height - + popoverSizes.marginTop - + popoverSizes.marginBottom; + } + if (offsetParent.offset().top + top < Dom.w.scrollTop()) { + // Correction for when the top is beyond the top of the page + top = buttonPosition.top + hostNode.height('outer'); + } + // Calculations all done - now set it + display.css({ + top: top + 'px', + left: left + 'px' }); - }, 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(); + } + else { + // Fix position - centre on screen + var place = function () { + var half = Dom.w.height() / 2; + var top = display.height() / 2; + if (top > half) { + top = half; } - }) - .on('keyup.dtb-collection', function (e) { - if (e.keyCode === 27) { + display.css('marginTop', top + 'px'); + }; + place(); + Dom.w.on('resize.dtb-collection', function () { + place(); + }); + } + fadeIn(display, options.fade); + if (options.background) { + Buttons.background(true, options.backgroundClassName, options.fade, options.backgroundHost || hostNode.get(0)); + } + // 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... + Dom.s('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.get(0) === hostNode.get(0)) { + return; + } 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(); + }); + }, 0); + } + display.trigger('buttons-popover.dt'); + dt.on('destroy.dtb-popover', close); + setTimeout(function () { + closed = false; + Dom.s('body') + .on('click.dtb-collection', function (e) { + if (closed) { + return; } - } - else { - if (active === elements.last()[0]) { - elements.first().focus(); + if ( + // Background click + Dom.s(e.target).classHas('dt-button-background') || + Dom.s(e.target).classHas('dtb-popover-close') || + // If not the current display element, or in the + // modal then somehow they have clicked behind the + // background + (e.target !== display.get(0) && + display.find(e.target).count() === 0)) { + 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 = content.find('a, button'); + var active = document.activeElement; + var first = elements.eq(0); + var last = elements.eq(elements.count() - 1); + if (e.keyCode !== 9) { + // tab + return; + } + if (elements.get().includes(active)) { + // If current focus is not inside the popover + 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]); + else if (e.shiftKey) { + // Reverse tabbing order when shift key is pressed + if (active === elements.get(0)) { + last.focus(); + e.preventDefault(); + } + } + else { + if (active === last.get(0)) { + first.focus(); + e.preventDefault(); + } + } + }); + }, 0); + } + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Public methods + */ + /** + * Get / set the action of a button + * @param node Button element + * @param action Function to set + * @return Self for chaining + */ + action(node, action) { + var button = this._nodeToButton(node); + if (action === undefined) { + return button ? button.conf.action : null; } - return; + if (button) { + button.conf.action = action; + } + return this; } - - if (typeof input === 'string') { - if (input.indexOf(',') !== -1) { - // String selector, list of names - process(input.split(',')); + /** + * Add an active class to the button to make to look active or get current + * active state. + * + * @param node Button element + * @param flag Enable / disable flag + * @return Self for chaining or boolean for getter + */ + active(node, flag) { + var button = this._nodeToButton(node); + var klass = this.c.dom.button.active; + if (button && + button.inCollection && + this.c.dom.collection.button && + this.c.dom.collection.button.active !== undefined) { + klass = this.c.dom.collection.button.active; } - else { - // String selector individual name - var idx = $.inArray(input.trim(), names); - - if (idx !== -1) { - ret.push(buttons[idx].inst); + if (flag === undefined) { + return button ? button.node.classHas(klass) : false; + } + if (button) { + button.node.classToggle(klass, flag === undefined ? true : flag); + } + return this; + } + /** + * Add a new button + * + * @param config Button configuration object, base string name or function + * @param idx Button index for where to insert the button + * @param draw Trigger a draw. Set a false when adding lots of buttons, + * until the last button. + * @return Self for chaining + */ + add(config, idx, draw = true) { + var buttons = this.s.buttons; + if (typeof idx === 'string') { + var parts = idx.split('-'); + var base = this.s; + for (var i = 0, ien = parts.length - 1; i < ien; i++) { + base = base.buttons[parseInt(parts[i])]; } + buttons = base.buttons; + idx = parseInt(parts[parts.length - 1]); } + let split = config ? config.split : undefined; + let node = this._expandButton(buttons, config, split, (!config || !split || split.length === 0) && base, false, idx); + if (draw === undefined || draw === true) { + this._draw(); + } + return node; } - else if (typeof input === 'number') { - // Index selector - ret.push(buttons[input].inst); + /** + * Clear buttons from a collection and then insert new buttons + */ + collectionRebuild(node, newButtons) { + var button = this._nodeToButton(node); + if (!button) { + return; + } + 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, undefined, newBtn.parentConf); + } + } + this._draw(button.collection, button.buttons); } - else if (typeof input === 'object') { - // Actual instance selector - ret.push(input); + /** + * Get the container node for the buttons + * + * @return Buttons node + */ + container() { + return this.dom.container; } - }; - - 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]; - + /** + * Disable a button + * @param node Button node + * @return Self for chaining + */ + disable(node) { + var button = this._nodeToButton(node); 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 + '-'); + if (button.isSplit) { + button.node + .children() + .eq(0) + .classAdd(this.c.dom.button.disabled) + .prop('disabled', true); } + else { + button.node + .classAdd(this.c.dom.button.disabled) + .prop('disabled', true); + } + button.disabled = true; + this._checkSplitEnable(); } + return this; } - }; - - 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 + /** + * Destroy the instance, cleaning up event handlers and removing DOM + * elements + * @return {Buttons} Self for chaining + */ + destroy() { + // Key event listener + Dom.s('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++) { - ret.push({ - inst: inst, - node: buttons[i].node - }); + this.remove(buttons[i].node.get(0)); } - } - else if (typeof selector === 'number') { - // Main button index selector - if (inst.s.buttons[selector]) { - ret.push({ - inst: inst, - node: inst.s.buttons[selector].node - }); + // Container + this.dom.container.remove(); + // Remove from the settings object collection + var buttonInsts = this.s.dt.settings()[0]._buttons; + for (i = 0, ien = buttonInsts.length; i < ien; i++) { + if (buttonInsts[i].inst === this) { + buttonInsts.splice(i, 1); + break; + } } + return this; } - 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); + /** + * Enable / disable a button + * + * @param node Button node + * @param flag Enable / disable flag + * @return Self for chaining + */ + enable(node, flag = true) { + if (flag === false) { + return this.disable(node); + } + var button = this._nodeToButton(node); + if (button) { + if (button.isSplit) { + button.node + .children() + .eq(0) + .classRemove(this.c.dom.button.disabled) + .prop('disabled', false); } + else { + button.node + .classRemove(this.c.dom.button.disabled) + .prop('disabled', false); + } + button.disabled = false; + this._checkSplitEnable(); } - 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 - }); + return this; + } + /** + * Get a button's index + * + * This is internally recursive + * + * @param node Button to get the index of + * @return Button index + */ + index(node, nested, buttons) { + if (!nested) { + nested = ''; } - 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 - }); + if (!buttons) { + buttons = this.s.buttons; + } + for (var i = 0, ien = buttons.length; i < ien; i++) { + var inner = buttons[i].buttons; + if (buttons[i].node.get(0) === node) { + return nested + i; + } + if (inner && inner.length) { + var match = this.index(node, i + '-', inner); + if (match !== null) { + return match; } } } - else { - // jQuery selector on the nodes - $(nodes) - .filter(selector) - .each(function () { - ret.push({ - inst: inst, - node: this - }); - }); - } + return null; } - else if (typeof selector === 'object' && selector.nodeName) { - // Node selector - var idx = $.inArray(selector, nodes); - - if (idx !== -1) { - ret.push({ - inst: inst, - node: nodes[idx] - }); + /** + * Get the instance name for the button set selector + * + * @return Instance name + */ + name() { + return this.c.name; + } + /** + * Get a button's node of the buttons container if no button is given + * @param node Button node + * @return Button element, or container + */ + node(node) { + if (!node) { + return this.dom.container; } + var button = this._nodeToButton(node); + return button ? button.node : null; } - }; - - 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) { - if (_entityDecoder) { - str = _entityDecoder(str); + /** + * Set / get a processing class on the selected button + * + * @param node Triggering button node + * @param flag true to add, false to remove, undefined to get + * @return Getter value or this if a setter. + */ + processing(node, flag) { + var dt = this.s.dt; + var button = this._nodeToButton(node); + if (flag === undefined) { + return button ? button.node.classHas('processing') : false; + } + if (button) { + button.node.classToggle('processing', flag); + Dom.s(dt.table().node()).trigger('buttons-processing.dt', false, [ + flag, + dt.button(node), + dt, + button.node, + button.conf + ]); + } + return this; } - else { - _exportTextarea.innerHTML = str; - str = _exportTextarea.value; + /** + * Remove a button. + * + * @param node Button node + * @return Self for chaining + */ + remove(node) { + let button = this._nodeToButton(node); + if (!button) { + return this; + } + let host = this._nodeToHost(node); + let dt = this.s.dt; + // Remove any child buttons first + if (button.buttons.length) { + for (let i = button.buttons.length - 1; i >= 0; i--) { + this.remove(button.buttons[i].node.get(0)); + } + } + button.conf.destroying = true; + // Allow the button to remove event handlers, etc + if (button.conf.destroy) { + button.conf.destroy.call(dt.button(node), dt, button.node, button.conf); + } + this._removeKey(button.conf); + button.node.remove(); + if (button.inserter) { + button.inserter.remove(); + } + if (host) { + let idx = host.indexOf(button); + host.splice(idx, 1); + } + return this; } - } - - return str; -}; - -/** - * Provide a custom entity decoding function - e.g. a regex one, which can be - * much faster than the built in DOM option, but also larger code size. - * @param {function} fn - */ -Buttons.entityDecoder = function (fn) { - _entityDecoder = fn; -} - -/** - * 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' + text(node, label) { + let button = this._nodeToButton(node); + if (!button) { + return label === undefined ? '' : this; } - // 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: '' + let textNode = button.textNode; + let dt = this.s.dt; + let text = function (opt) { + return typeof opt === 'function' + ? opt(dt, button.node, button.conf) + : opt; + }; + if (label === undefined) { + return text(button.conf.text || ''); } - }, - 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' + button.conf.text = label; + if (textNode) { + textNode.html(text(label)); } + return this; } - } -}; - -/** - * Version information - * @type {string} - * @static - */ -Buttons.version = '2.4.2'; - -$.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); + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Constructor + */ + constructor(dtIn, configIn) { + let config; + let dt = new DataTable.Api(dtIn); + // If there is no config set it to an empty object + if (typeof configIn === 'undefined') { + config = {}; + } + else if (configIn === true) { + // Allow a boolean true for defaults + config = {}; + } + else if (Array.isArray(configIn)) { + // For easy configuration of buttons an array can be given + config = { buttons: configIn }; } else { - this.popover(config._collection, config); + config = configIn; } - - // 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(); + this.c = util.object.assignDeep({}, Buttons.defaults, config); + // Don't want a deep copy for the buttons + if (config.buttons) { + this.c.buttons = config.buttons; } - }, - 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' + this.s = { + dt, + buttons: [], + listenKeys: '', + namespace: 'dtb' + _instCounter++ + }; + this.dom = { + container: Dom.c(this.c.dom.container.tag).classAdd(this.c.dom.container.className) + }; + var that = this; + var dtSettings = this.s.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 + Dom.c('body').on('keyup.' + this.s.namespace, function (e) { + if (!document.activeElement || + document.activeElement === document.body) { + // Use 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); + } + } + }); } - // Also the popover options, defined in Buttons.popover - }, - copy: function (dt, conf) { - if (_dtButtons.copyHtml5) { - return 'copyHtml5'; + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Private methods + */ + /** + * Add a new button to the key press listener + * @param conf Resolved button configuration object + */ + _addKey(conf) { + if (conf.key) { + this.s.listenKeys += util.is.plainObject(conf.key) + ? conf.key.key + : conf.key; + } } - }, - csv: function (dt, conf) { - if (_dtButtons.csvHtml5 && _dtButtons.csvHtml5.available(dt, conf)) { - return 'csvHtml5'; + /** + * Insert the buttons into the container. Call without parameters! + * + * @param container Recursive only - Insert point + * @param buttons Recursive only - Buttons array + */ + _draw(container, buttons) { + if (!container) { + container = this.dom.container; + } + if (!buttons) { + buttons = this.s.buttons; + } + container.children().detach(); + for (var i = 0, ien = buttons.length; i < ien; i++) { + container.append(buttons[i].inserter); + container.append(document.createTextNode(' ')); + if (buttons[i].buttons && buttons[i].buttons.length) { + this._draw(buttons[i].collection, buttons[i].buttons); + } + } } - }, - excel: function (dt, conf) { - if (_dtButtons.excelHtml5 && _dtButtons.excelHtml5.available(dt, conf)) { - return 'excelHtml5'; + /** + * Create buttons from an array of buttons + */ + _expandButton(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; + var lastButton; + if (button === undefined) { + buttons = !Array.isArray(split) ? [split] : split; + } + for (var i = 0, ien = buttons.length; i < ien; i++) { + // If the configuration is an array, then expand the buttons at this + // point + if (Array.isArray(buttons[i])) { + this._expandButton(attachTo, buttons[i], undefined, inCollection, parentConf !== undefined && parentConf.split !== undefined, attachPoint, parentConf); + continue; + } + var conf = this._resolveExtends(buttons[i]); + if (!conf) { + continue; + } + isSplit = conf.split ? true : false; + // If the configuration is an array, then expand the buttons at this + // point + if (Array.isArray(conf)) { + this._expandButton(attachTo, conf, false, inCollection, parentConf !== undefined && parentConf.split !== undefined, attachPoint, parentConf); + continue; + } + var built = this._buildButton(conf, inCollection, conf.split !== undefined, inSplit); + if (!built) { + continue; + } + if (attachPoint !== undefined && attachPoint !== null) { + attachTo.splice(attachPoint, 0, built); + attachPoint++; + } + else { + attachTo.push(built); + } + // Any button type can have a drop icon set + if (built.conf.dropIcon && !built.conf.split) { + built.node + .classAdd(this.c.dom.button.dropClass) + .append(this.c.dom.button.dropHtml); + } + // Create the dropdown for a collection + if (built.conf.buttons) { + built.collection = Dom.c(domCollection.container.content.tag); + built.conf._collection = built.collection; + 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 = Dom.c(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); + } + lastButton = built.node; + } + return lastButton; } - }, - pdf: function (dt, conf) { - if (_dtButtons.pdfHtml5 && _dtButtons.pdfHtml5.available(dt, conf)) { - return 'pdfHtml5'; + /** + * Create an individual button + * + * @param config Resolved button configuration + * @param inCollection `true` if a collection button + * @param isSplit Is a split button + * @param inSplit Is a part of a split button + * @return {object} Completed button description object + */ + _buildButton(config, inCollection, isSplit, inSplit) { + var that = this; + var configDom = this.c.dom; + var textNode = null; + var dt = this.s.dt; + var setLinerTab = false; + 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 domStructure = util.object.assignDeep({}, configDom.button); + if (inCollection && isSplit && configDom.collection.split) { + util.object.assignDeep(domStructure, configDom.collection.split.action); + } + else if (inSplit || inCollection) { + util.object.assignDeep(domStructure, configDom.collection.button); + } + else if (isSplit) { + util.object.assignDeep(domStructure, configDom.split.button); + } + // Spacers don't do much other than insert an element into the DOM + if (config.spacer) { + var spacer = Dom.c(domStructure.spacer.tag) + .classAdd([ + 'dt-button-spacer', + config.style || '', + domStructure.spacer.className + ]) + .html(text(config.text || '')); + return { + buttons: [], + collection: null, + conf: config, + disabled: false, + inserter: spacer, + inSplit: inSplit, + inCollection: inCollection, + isCollection: false, + isSplit: false, + node: spacer, + nodeChild: 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.html) { + return false; + } + var button; + if (!config.html) { + var run = function (e, dt, button, config, done) { + var _a; + (_a = config.action) === null || _a === void 0 ? void 0 : _a.call(dt.button(button), e, dt, button, config, done); + Dom.s(dt.table().node()).trigger('buttons-action.dt', false, [ + dt.button(button), + dt, + button, + config + ]); + }; + var action = function (e, dt, button, config) { + if (config.async) { + that.processing(button.get(0), true); + setTimeout(function () { + run(e, dt, button, config, function () { + that.processing(button.get(0), false); + }); + }, config.async); + } + else { + run(e, dt, button, config, () => { }); + } + }; + var tag = config.tag || domStructure.tag; + var clickBlurs = config.clickBlurs === undefined ? true : config.clickBlurs; + button = Dom.c(tag) + .classAdd(domStructure.className) + .attr('aria-controls', this.s.dt.table().node().id) + .on('click.dtb', function (e) { + e.preventDefault(); + if (!button.classHas(domStructure.disabled) && + config.action) { + action(e, dt, button, config, () => { }); + } + if (clickBlurs) { + button.blur(); + } + }) + .on('keypress.dtb', function (e) { + if (e.keyCode === 13) { + e.preventDefault(); + if (!button.classHas(domStructure.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 (domStructure.liner.tag) { + var lc = domStructure.liner.tag.toLowerCase(); + var liner = Dom.c(lc) + .html(text(config.text || '')) + .classAdd(domStructure.liner.className); + if (lc === 'a') { + liner.attr('href', '#'); + } + if (lc === 'a' || lc === 'button') { + liner.attr('tabindex', this.s.dt.settings()[0].tabIndex); + setLinerTab = true; + } + button.append(liner); + textNode = liner; + } + else { + button.html(text(config.text || '')); + textNode = button; + } + if (!setLinerTab) { + button.attr('tabindex', this.s.dt.settings()[0].tabIndex); + } + if (config.enabled === false) { + button.classAdd(domStructure.disabled); + } + if (config.className) { + button.classAdd(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++; + } + } + else { + button = Dom.c('div').html(config.html); + } + var buttonContainer = this.c.dom.buttonContainer; + var inserter; + if (buttonContainer && buttonContainer.tag) { + inserter = Dom.c(buttonContainer.tag) + .classAdd(buttonContainer.className) + .append(button); + } + else { + inserter = button; + } + this._addKey(config); + // Style integration callback for DOM manipulation + if (this.c.buttonCreated) { + inserter = this.c.buttonCreated(config, inserter, inCollection); + } + var splitDiv; + if (isSplit) { + var dropdownConf = inCollection + ? util.object.assignDeep(this.c.dom.split, this.c.dom.collection.split) + : this.c.dom.split; + var wrapperConf = dropdownConf.wrapper; + splitDiv = Dom.c(wrapperConf.tag) + .classAdd(wrapperConf.className) + .append(button); + var dropButtonConfig = util.object.assign(config, { + autoClose: true, + 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) { + if (typeof Buttons.buttons.split === 'object' && + Buttons.buttons.split.action) { + Buttons.buttons.split.action.call(dt.button(splitDiv), e, dt, button, config, () => { }); + } + Dom.s(dt.table().node()).trigger('buttons-action.dt', false, [ + dt.button(button), + dt, + button, + config + ]); + button.attr('aria-expanded', true); + }; + var dropButton = Dom.c('button') + .classAdd([ + dropdownConf.dropdown.className, + 'dt-button', + this.c.dom.button.dropClass + ]) + .html(this.c.dom.button.dropHtml) + .on('click.dtb', function (e) { + e.preventDefault(); + e.stopPropagation(); + if (!dropButton.classHas(domStructure.disabled)) { + splitAction(e, dt, dropButton, dropButtonConfig, () => { }); + } + if (clickBlurs) { + dropButton.blur(); + } + }) + .on('keypress.dtb', function (e) { + if (e.keyCode === 13) { + e.preventDefault(); + if (!dropButton.classHas(domStructure.disabled)) { + splitAction(e, dt, dropButton, dropButtonConfig, () => { }); + } + } + }); + if (config.split && config.split.length === 0) { + dropButton.classAdd('dtb-hide-drop'); + } + if (dropButtonConfig.attr) { + splitDiv.append(dropButton).attr(dropButtonConfig.attr); + } + button = splitDiv; + } + return { + buttons: [], + collection: null, + conf: config, + disabled: false, + inserter: isSplit ? button : inserter, + inCollection: inCollection, + inSplit: inSplit, + isCollection: false, + isSplit: isSplit, + node: button, + nodeChild: button && button.children && button.children().count() + ? button.children().get(0) + : null, + textNode: textNode + }; } - }, - 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]; + /** + * Spin over buttons checking if splits should be enabled or not. + * @param buttons Array of buttons to check + */ + _checkSplitEnable(buttons) { + if (!buttons) { + buttons = this.s.buttons; + } + for (var i = 0; i < buttons.length; i++) { + var button = buttons[i]; + // Check if the button is a split one and if so, determine + // its state + if (button.isSplit) { + var splitBtn = button.node.get(0).childNodes[1]; + if (this._checkAnyEnabled(button.buttons)) { + // Enable the split + Dom.s(splitBtn) + .classRemove(this.c.dom.button.disabled) + .prop('disabled', false); + } + else { + Dom.s(splitBtn) + .classAdd(this.c.dom.button.disabled) + .prop('disabled', false); + } + } + else if (button.isCollection) { + // Nest down into collections + this._checkSplitEnable(button.buttons); + } + } + } + /** + * Check an array of buttons and see if any are enabled in it + * + * @param buttons Button array + * @returns true if a button is enabled, false otherwise + */ + _checkAnyEnabled(buttons) { + for (var i = 0; i < buttons.length; i++) { + if (!buttons[i].disabled) { + return true; + } + } + return false; + } + /** + * Get the button object from a node (recursive) + * + * @param node Button node + * @param buttons Button array, uses base if not defined + * @return Button object + */ + _nodeToButton(node, buttons) { + if (!buttons) { + buttons = this.s.buttons; + } + if (util.is.dom(node)) { + node = node.get(0); + } + for (var i = 0, ien = buttons.length; i < ien; i++) { + if (buttons[i].node.get(0) === node || + buttons[i].nodeChild === 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 Button node + * @param buttons Button array, uses base if not defined + * @return Button's host array + */ + _nodeToHost(node, buttons) { + if (!buttons) { + buttons = this.s.buttons; + } + if (util.is.dom(node)) { + node = node.get(0); + } + for (var i = 0, ien = buttons.length; i < ien; i++) { + if (buttons[i].node.get(0) === 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 character The character pressed + * @param e Key event that triggered this call + */ + _keypress(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.trigger('click'); + } + else if (util.is.plainObject(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.trigger('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 conf Button configuration + */ + _removeKey(conf) { + if (conf.key) { + var character = util.is.plainObject(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 = a.indexOf(character); + a.splice(idx, 1); + this.s.listenKeys = a.join(''); + } + } + /** + * Resolve a button configuration + * + * @param confIn Button config to resolve + * @return Button configuration + */ + _resolveExtends(confIn) { + var that = this; + var dt = this.s.dt; + var i, ien; + var toConfObject = function (base, lastConfig) { + var loop = 0; + // Loop until we have resolved to a button configuration, or an + // array of button configurations (which will be iterated + // separately) + while (!util.is.plainObject(base) && !Array.isArray(base)) { + if (base === undefined) { + return false; + } + if (typeof base === 'function') { + base = base.call(that, dt, lastConfig); + if (!base) { + return false; + } + } + else if (typeof base === 'string') { + if (!Buttons.buttons[base]) { + return { html: base }; + } + base = Buttons.buttons[base]; + } + loop++; + if (loop > 30) { + // Protect against misconfiguration killing the browser + throw 'Buttons: Too many iterations'; + } + } + return Array.isArray(base) ? base : util.object.assign({}, base); + }; + var confRes = toConfObject(confIn, confIn); + if (confRes === false) { + return false; + } + var conf = confRes; + while (conf && conf.extend) { + // Use `toConfObject` in case the button definition being extended + // is itself a string or a function + if (!Buttons.buttons[conf.extend]) { + throw 'Cannot extend unknown button type: ' + conf.extend; + } + var objArray = toConfObject(Buttons.buttons[conf.extend], conf); + if (Array.isArray(objArray)) { + return objArray; + } + 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; + conf = util.object.assign({}, 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; + } + } + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Statics + */ + /** + * Show / hide a background layer behind a collection + * + * @param show Flag to indicate if the background should be shown or + * hidden + * @param className Class to assign to the background + * @param fade Fade time + * @param insertPoint Define the insert point + */ + Buttons.background = function (show, className = '', fade = 400, insertPoint = document.body) { + if (show) { + let div = Dom.c('div').classAdd(className).css('opacity', '0'); + if (insertPoint === document.body) { + div.appendTo(insertPoint); + } + else { + div.insertAfter(insertPoint); + } + fadeIn(div, fade); } 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); + fadeOut(Dom.s('div.' + className), fade, function () { + this.classRemove(className).remove(); + }); + } + }; + Buttons.buttons = DataTable.ext.buttons; + /** + * 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 group Instance selector - see `instance-selector` documentation on + * the DataTables site + * @param Button instance array that was attached to the DataTables settings + * object + * @return Buttons instances + */ + Buttons.instanceSelector = function (group, buttons) { + // If no group selector, then return all + if (group === undefined || group === null) { + return buttons.map(v => v.inst); + } + var ret = []; + var names = buttons.map(v => 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 { - vals.push(option); - lang.push(option); + // String selector individual name + var idx = names.indexOf(input.trim()); + if (idx !== -1) { + ret.push(buttons[idx].inst); + } + } + } + else if (typeof input === 'number') { + // Index selector + ret.push(buttons[input].inst); + } + else if (util.is.element(input)) { + // Element selector + for (var j = 0; j < buttons.length; j++) { + if (buttons[j].inst.dom.container.get(0) === input) { + ret.push(buttons[j].inst); + } + } + } + else if (typeof input === 'object') { + // Actual instance selector + ret.push(input); + } + }; + process(group); + return ret; + }; + /** + * Button selector - select one or more buttons from a selector input so some + * operation can be performed on them. + * @param Button instances array that the selector should operate on + * @param Button selector - see + * `button-selector` documentation on the DataTables site + * @return Array of objects containing `inst` and `idx` properties of + * the selected buttons so you know which instance each button belongs to. + */ + 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 = buttons.map(v => v.node.get(0)); + if (util.is.dom(selector)) { + selector = selector.get(); + } + if (selector && util.is.arrayLike(selector)) { + 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 = buttons.map(v => v.idx); + ret.push({ + inst: inst, + node: buttons[indexes.indexOf(selector)].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 { + // CSS selector on the nodes + Dom.s(nodes) + .filter(selector) + .each(function (el) { + ret.push({ + inst: inst, + node: Dom.s(el) + }); + }); + } + } + else if (util.is.element(selector)) { + // Node selector + var idx = nodes.indexOf(selector); + if (idx !== -1) { + ret.push({ + inst: inst, + node: Dom.s(nodes[idx]) + }); + } + } + }; + for (var i = 0, ien = insts.length; i < ien; i++) { + var inst = insts[i]; + run(selector, inst); } - - 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(); + return ret; + }; + /** + * Default function used for formatting output data. + * + * @param str Data to strip + */ + Buttons.stripData = function (input, config) { + // If the input is an HTML element, we can use the HTML from it (HTML + // might be stripped below). + var str = input !== null && + typeof input === 'object' && + input.nodeName && + input.nodeType + ? input.innerHTML + : input; + if (typeof str !== 'string') { + return str; + } + // Always remove script tags + str = Buttons.stripHtmlScript(str); + // Always remove comments + str = Buttons.stripHtmlComments(str); + if (!config || config.stripHtml) { + str = DataTable.util.stripHtml(str); + } + if (!config || config.trim) { + str = str.trim(); + } + if (!config || config.stripNewlines) { + str = str.replace(/\n/g, ' '); + } + if (!config || config.decodeEntities) { + if (_entityDecoder) { + str = _entityDecoder(str); + } + else { + _exportTextarea.innerHTML = str; + str = _exportTextarea.value; + } + } + // Prevent Excel from running a formula + if (!config || config.escapeExcelFormula) { + if (str.match(/^[=@\t\r]/)) { + str = "'" + str; + } + } + return str; + }; + /** + * Provide a custom entity decoding function - e.g. a regex one, which can + * be much faster than the built in DOM option, but also larger code size. + * + * @param fn + */ + Buttons.entityDecoder = function (fn) { + _entityDecoder = fn; + }; + /** + * Common function for stripping HTML comments + * + * @param input + * @returns + */ + Buttons.stripHtmlComments = function (input) { + var previous; + do { + previous = input; + input = input.replace(/ - + diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tables.ftl b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tables.ftl index 3fcc6fe398b..b80058223fa 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tables.ftl +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tables.ftl @@ -27,7 +27,7 @@ */ $(function () { - tableList = $('#tableList').DataTable({ + tableList = new DataTable('#tableList', { "ajax": { "url": '${rootContext}rest-v2/tables', "dataSrc": function (json) {