|
19 | 19 | @endpush |
20 | 20 |
|
21 | 21 | <script> |
| 22 | +/* eslint-disable */ |
| 23 | + (function($) { |
| 24 | + if (!$ || !$.fn || !$.fn.dataTable || !$.fn.dataTable.FixedHeader) { |
| 25 | + return; |
| 26 | + } |
| 27 | +
|
| 28 | + const proto = $.fn.dataTable.FixedHeader.prototype; |
| 29 | + if (!proto || proto._backpackNoBelowPatchApplied) { |
| 30 | + return; |
| 31 | + } |
| 32 | +
|
| 33 | + const originalModeChange = proto._modeChange; |
| 34 | + proto._modeChange = function(mode, type) { |
| 35 | + const args = Array.prototype.slice.call(arguments); |
| 36 | + if (type === 'header' && args[0] === 'below' && this && this.c && this.c.headerOffset > 0) { |
| 37 | + args[0] = 'in-place'; |
| 38 | + } |
| 39 | +
|
| 40 | + const result = originalModeChange.apply(this, args); |
| 41 | +
|
| 42 | + return result; |
| 43 | + }; |
| 44 | +
|
| 45 | + proto._backpackNoBelowPatchApplied = true; |
| 46 | + })(window.jQuery); |
| 47 | +
|
22 | 48 | // Store the alerts in localStorage for this page |
23 | 49 | let $oldAlerts = JSON.parse(localStorage.getItem('backpack_alerts')) |
24 | 50 | ? JSON.parse(localStorage.getItem('backpack_alerts')) : {}; |
@@ -198,6 +224,12 @@ functionsToRunOnDataTablesDrawEvent: [], |
198 | 224 | config.lineButtonsAsDropdownMinimum = parseInt(tableElement.getAttribute('data-line-buttons-as-dropdown-minimum')) ?? 3; |
199 | 225 | config.lineButtonsAsDropdownShowBeforeDropdown = parseInt(tableElement.getAttribute('data-line-buttons-as-dropdown-show-before-dropdown')) ?? 1; |
200 | 226 | config.responsiveTable = tableElement.getAttribute('data-responsive-table') === 'true' || tableElement.getAttribute('data-responsive-table') === '1'; |
| 227 | + const useFixedHeaderAttr = tableElement.getAttribute('data-use-fixed-header'); |
| 228 | + if (useFixedHeaderAttr === null || useFixedHeaderAttr === '') { |
| 229 | + config.useFixedHeader = config.responsiveTable; |
| 230 | + } else { |
| 231 | + config.useFixedHeader = useFixedHeaderAttr.toLowerCase() === 'true'; |
| 232 | + } |
201 | 233 | config.exportButtons = tableElement.getAttribute('data-has-export-buttons') === 'true'; |
202 | 234 | // Apply any custom config |
203 | 235 | if (customConfig && Object.keys(customConfig).length > 0) { |
@@ -261,10 +293,14 @@ functionsToRunOnDataTablesDrawEvent: [], |
261 | 293 | } |
262 | 294 | |
263 | 295 | // Create DataTable configuration |
| 296 | + const initialFixedHeaderOffset = calculateStickyHeaderOffset(tableElement); |
264 | 297 | const dataTableConfig = { |
265 | 298 | bInfo: config.showEntryCount, |
266 | 299 | responsive: config.responsiveTable, |
267 | | - fixedHeader: config.responsiveTable, |
| 300 | + fixedHeader: config.useFixedHeader ? { |
| 301 | + header: true, |
| 302 | + headerOffset: initialFixedHeaderOffset |
| 303 | + } : false, |
268 | 304 | scrollX: !config.responsiveTable, |
269 | 305 | autoWidth: false, |
270 | 306 | processing: true, |
@@ -877,6 +913,242 @@ function resizeCrudTableColumnWidths() { |
877 | 913 | resizeCrudTableColumnWidths(); |
878 | 914 | }); |
879 | 915 | } |
| 916 | +
|
| 917 | + registerFixedHeaderListeners(tableId, config); |
| 918 | +} |
| 919 | +
|
| 920 | +function resolveFixedHeaderOffset(fixedHeader, explicitOffset) { |
| 921 | + if (typeof explicitOffset === 'number') { |
| 922 | + return explicitOffset; |
| 923 | + } |
| 924 | +
|
| 925 | + if (!fixedHeader) { |
| 926 | + return 0; |
| 927 | + } |
| 928 | +
|
| 929 | + if (typeof fixedHeader.headerOffset === 'function') { |
| 930 | + const value = fixedHeader.headerOffset(); |
| 931 | + if (typeof value === 'number') { |
| 932 | + return value; |
| 933 | + } |
| 934 | + } |
| 935 | +
|
| 936 | + if (fixedHeader.c && typeof fixedHeader.c.headerOffset === 'number') { |
| 937 | + return fixedHeader.c.headerOffset; |
| 938 | + } |
| 939 | +
|
| 940 | + return 0; |
| 941 | +} |
| 942 | +
|
| 943 | +function measureFixedHeaderHeight(fixedHeader, headerElement) { |
| 944 | + const storedHeight = fixedHeader && fixedHeader.s && typeof fixedHeader.s.headerHeight === 'number' |
| 945 | + ? Math.max(0, Math.round(fixedHeader.s.headerHeight)) |
| 946 | + : 0; |
| 947 | +
|
| 948 | + if (storedHeight > 0) { |
| 949 | + return storedHeight; |
| 950 | + } |
| 951 | +
|
| 952 | + if (headerElement) { |
| 953 | + const rectHeight = Math.max(0, Math.round(headerElement.getBoundingClientRect().height)); |
| 954 | + if (rectHeight > 0) { |
| 955 | + return rectHeight; |
| 956 | + } |
| 957 | +
|
| 958 | + const offsetHeight = Math.max(0, Math.round(headerElement.offsetHeight || 0)); |
| 959 | + if (offsetHeight > 0) { |
| 960 | + return offsetHeight; |
| 961 | + } |
| 962 | + } |
| 963 | +
|
| 964 | + return 56; |
| 965 | +} |
| 966 | +
|
| 967 | +function deriveFixedHeaderMargins(headerHeight) { |
| 968 | + const enableMargin = Math.max(10, headerHeight ? Math.round(Math.max(14, headerHeight * 0.35)) : 28); |
| 969 | + const disableMargin = Math.max(enableMargin + 14, headerHeight ? Math.round(Math.max(24, headerHeight * 0.6)) : 44); |
| 970 | + return { enableMargin, disableMargin }; |
| 971 | +} |
| 972 | +
|
| 973 | +function registerFixedHeaderListeners(tableId, config) { |
| 974 | + if (!config.useFixedHeader || config.fixedHeaderListenersRegistered) { |
| 975 | + return; |
| 976 | + } |
| 977 | +
|
| 978 | + const tableElement = document.getElementById(tableId); |
| 979 | + const apiInstance = window.crud.tables[tableId]; |
| 980 | + const fixedHeader = apiInstance && apiInstance.fixedHeader; |
| 981 | +
|
| 982 | + if (!tableElement || !fixedHeader || typeof fixedHeader.headerOffset !== 'function' || typeof fixedHeader.enabled !== 'function') { |
| 983 | + return; |
| 984 | + } |
| 985 | +
|
| 986 | + const headerElement = tableElement.querySelector('thead'); |
| 987 | + const state = { |
| 988 | + timer: null, |
| 989 | + lastOffset: null, |
| 990 | + lastEnabled: null, |
| 991 | + listeners: [] |
| 992 | + }; |
| 993 | +
|
| 994 | + const ensureActivation = (explicitOffset) => { |
| 995 | + const offsetValue = resolveFixedHeaderOffset(fixedHeader, explicitOffset); |
| 996 | + const rect = tableElement.getBoundingClientRect(); |
| 997 | + const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; |
| 998 | + const currentlyEnabled = fixedHeader.enabled(); |
| 999 | + const headerHeight = measureFixedHeaderHeight(fixedHeader, headerElement); |
| 1000 | + const { enableMargin, disableMargin } = deriveFixedHeaderMargins(headerHeight); |
| 1001 | +
|
| 1002 | + const withinViewport = rect.top < viewportHeight - 1 && rect.bottom > offsetValue + 1; |
| 1003 | + if (!withinViewport) { |
| 1004 | + if (currentlyEnabled) { |
| 1005 | + fixedHeader.disable(); |
| 1006 | + } |
| 1007 | + return false; |
| 1008 | + } |
| 1009 | +
|
| 1010 | + const headerBottom = rect.top + headerHeight; |
| 1011 | + const clearanceThreshold = currentlyEnabled ? offsetValue + disableMargin : offsetValue - enableMargin; |
| 1012 | + const shouldEnable = headerBottom <= clearanceThreshold; |
| 1013 | +
|
| 1014 | + if (shouldEnable === currentlyEnabled) { |
| 1015 | + return shouldEnable; |
| 1016 | + } |
| 1017 | +
|
| 1018 | + if (shouldEnable) { |
| 1019 | + fixedHeader.enable(true); |
| 1020 | + } else { |
| 1021 | + fixedHeader.disable(); |
| 1022 | + } |
| 1023 | +
|
| 1024 | + return shouldEnable; |
| 1025 | + }; |
| 1026 | +
|
| 1027 | + const recalculate = (reason) => { |
| 1028 | + const offset = calculateStickyHeaderOffset(tableElement); |
| 1029 | + const enabled = ensureActivation(offset); |
| 1030 | + const offsetChanged = typeof state.lastOffset !== 'number' || state.lastOffset !== offset; |
| 1031 | + const enabledChanged = typeof state.lastEnabled !== 'boolean' || state.lastEnabled !== enabled; |
| 1032 | +
|
| 1033 | + if (offsetChanged) { |
| 1034 | + fixedHeader.headerOffset(offset); |
| 1035 | + } |
| 1036 | +
|
| 1037 | + if (enabled && (offsetChanged || enabledChanged || /(?:dt:|window:resize|orientationchange)/.test(reason || ''))) { |
| 1038 | + if (typeof fixedHeader.adjust === 'function') { |
| 1039 | + fixedHeader.adjust(); |
| 1040 | + } |
| 1041 | + } |
| 1042 | +
|
| 1043 | + state.lastOffset = offset; |
| 1044 | + state.lastEnabled = enabled; |
| 1045 | + }; |
| 1046 | +
|
| 1047 | + const scheduleRecalculation = (reason) => { |
| 1048 | + if (state.timer) { |
| 1049 | + return; |
| 1050 | + } |
| 1051 | +
|
| 1052 | + state.timer = setTimeout(() => { |
| 1053 | + state.timer = null; |
| 1054 | + recalculate(reason || 'timer'); |
| 1055 | + }, 75); |
| 1056 | + }; |
| 1057 | +
|
| 1058 | + const addListener = (target, eventName, handler) => { |
| 1059 | + if (!target || !target.addEventListener) { |
| 1060 | + return; |
| 1061 | + } |
| 1062 | + target.addEventListener(eventName, handler, false); |
| 1063 | + state.listeners.push(() => target.removeEventListener(eventName, handler, false)); |
| 1064 | + }; |
| 1065 | +
|
| 1066 | + recalculate('initial'); |
| 1067 | + setTimeout(() => recalculate('delayed-initial'), 150); |
| 1068 | +
|
| 1069 | + addListener(window, 'resize', () => scheduleRecalculation('window:resize')); |
| 1070 | + addListener(window, 'orientationchange', () => scheduleRecalculation('window:orientationchange')); |
| 1071 | + addListener(window, 'scroll', () => scheduleRecalculation('window:scroll')); |
| 1072 | +
|
| 1073 | + const $table = $(`#${tableId}`); |
| 1074 | + $table.on('column-visibility.dt.fixedHeader length.dt.fixedHeader responsive-resize.fixedHeader draw.dt.fixedHeader', function(evt) { |
| 1075 | + const eventLabel = evt && evt.type ? 'dt:' + evt.type : 'dt:unknown'; |
| 1076 | + scheduleRecalculation(eventLabel); |
| 1077 | + }); |
| 1078 | +
|
| 1079 | + $table.on('destroy.dt.fixedHeader', function() { |
| 1080 | + if (state.timer) { |
| 1081 | + clearTimeout(state.timer); |
| 1082 | + state.timer = null; |
| 1083 | + } |
| 1084 | +
|
| 1085 | + state.listeners.forEach(function(cleanup) { |
| 1086 | + cleanup(); |
| 1087 | + }); |
| 1088 | + state.listeners.length = 0; |
| 1089 | +
|
| 1090 | + $table.off('.fixedHeader'); |
| 1091 | + config.fixedHeaderListenersRegistered = false; |
| 1092 | + }); |
| 1093 | +
|
| 1094 | + config.fixedHeaderListenersRegistered = true; |
| 1095 | +} |
| 1096 | +
|
| 1097 | +function calculateStickyHeaderOffset(tableElement) { |
| 1098 | + if (!tableElement || tableElement.closest('.modal')) { |
| 1099 | + return 0; |
| 1100 | + } |
| 1101 | +
|
| 1102 | + if (typeof document.elementsFromPoint !== 'function') { |
| 1103 | + return 0; |
| 1104 | + } |
| 1105 | +
|
| 1106 | + const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0; |
| 1107 | + const sampleX = Math.max(0, Math.round(viewportWidth / 2)); |
| 1108 | + const maxScanDepth = Math.min(400, Math.max(200, (window.innerHeight || 0) / 2)); |
| 1109 | + const seenElements = new Set(); |
| 1110 | + let offset = 0; |
| 1111 | +
|
| 1112 | + for (let y = 0; y <= maxScanDepth; y += 8) { |
| 1113 | + const elements = document.elementsFromPoint(sampleX, y) || []; |
| 1114 | +
|
| 1115 | + elements.forEach((element) => { |
| 1116 | + if (!element || seenElements.has(element)) { |
| 1117 | + return; |
| 1118 | + } |
| 1119 | +
|
| 1120 | + seenElements.add(element); |
| 1121 | +
|
| 1122 | + if (element.closest('.dtfh-floatingparent')) { |
| 1123 | + return; |
| 1124 | + } |
| 1125 | +
|
| 1126 | + const computedStyle = window.getComputedStyle(element); |
| 1127 | + if (computedStyle.position !== 'sticky' && computedStyle.position !== 'fixed') { |
| 1128 | + return; |
| 1129 | + } |
| 1130 | +
|
| 1131 | + const rect = element.getBoundingClientRect(); |
| 1132 | + if (rect.bottom <= 0) { |
| 1133 | + return; |
| 1134 | + } |
| 1135 | +
|
| 1136 | + const topValue = parseFloat(computedStyle.top) || 0; |
| 1137 | + if (topValue > y + 2) { |
| 1138 | + return; |
| 1139 | + } |
| 1140 | +
|
| 1141 | + offset = Math.max(offset, rect.bottom); |
| 1142 | + }); |
| 1143 | +
|
| 1144 | + if (offset > 0 && y > offset) { |
| 1145 | + break; |
| 1146 | + } |
| 1147 | + } |
| 1148 | +
|
| 1149 | + const finalOffset = Math.max(0, Math.round(offset)); |
| 1150 | +
|
| 1151 | + return finalOffset; |
880 | 1152 | } |
881 | 1153 |
|
882 | 1154 | // Support for multiple tables with filters |
|
0 commit comments