funda-ui 4.7.730 → 4.7.740

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -247,10 +247,12 @@ const EventCalendarTimeline = (props: EventCalendarTimelineProps) => {
247
247
  //
248
248
  const now = useMemo(() => new Date(), []);
249
249
  const [date, setDate] = useState<Date>(now);
250
+ const prevDateRef = useRef<Date | null>(null);
250
251
  const [day, setDay] = useState<number>(date.getDate());
251
252
  const [month, setMonth] = useState<number>(date.getMonth());
252
253
  const [year, setYear] = useState<number>(date.getFullYear());
253
254
  const [startDay, setStartDay] = useState<number>(getStartDayOfMonth(date));
255
+
254
256
 
255
257
 
256
258
  // selection tab
@@ -291,6 +293,10 @@ const EventCalendarTimeline = (props: EventCalendarTimelineProps) => {
291
293
  const [tempDate, setTempDate] = useState<string>('');
292
294
 
293
295
 
296
+ // Track all pending requestAnimationFrame IDs for cleanup on unmount
297
+ const rafIdsRef = useRef<Set<number>>(new Set());
298
+
299
+
294
300
 
295
301
  // Open temporary storage for pop-ups
296
302
  const [tableRowNum, setTableRowNum] = useState<number>(-1);
@@ -901,6 +907,7 @@ const EventCalendarTimeline = (props: EventCalendarTimelineProps) => {
901
907
  let mouseDown: boolean = false;
902
908
  let startX: any = 0;
903
909
  let scrollLeft: any = 0;
910
+ const tableDragRafIdRef = useRef<number | null>(null);
904
911
 
905
912
  const handleTableDragStart = useCallback((e: any) => {
906
913
  draggedObj = e.currentTarget;
@@ -920,14 +927,29 @@ const EventCalendarTimeline = (props: EventCalendarTimelineProps) => {
920
927
 
921
928
  mouseDown = false;
922
929
  draggedObj.classList.remove('dragging');
930
+
931
+ // Cancel any pending animation frame
932
+ if (tableDragRafIdRef.current !== null) {
933
+ cancelAnimationFrame(tableDragRafIdRef.current);
934
+ tableDragRafIdRef.current = null;
935
+ }
936
+
923
937
  }, []);
924
938
 
925
939
  const handleTableMove = useCallback((e: any) => {
926
940
  e.preventDefault();
927
941
  if (!mouseDown) { return; }
928
- const x = e.pageX - draggedObj.offsetLeft;
929
- const scroll = x - startX;
930
- draggedObj.scrollLeft = scrollLeft - scroll;
942
+
943
+
944
+ // Use requestAnimationFrame to throttle scroll updates for better performance
945
+ if (tableDragRafIdRef.current === null) {
946
+ tableDragRafIdRef.current = requestAnimationFrame(() => {
947
+ const x = e.pageX - draggedObj.offsetLeft;
948
+ const scroll = x - startX;
949
+ draggedObj.scrollLeft = scrollLeft - scroll;
950
+ tableDragRafIdRef.current = null;
951
+ });
952
+ }
931
953
  }, []);
932
954
 
933
955
 
@@ -1019,25 +1041,37 @@ const EventCalendarTimeline = (props: EventCalendarTimelineProps) => {
1019
1041
  // ================================================================
1020
1042
  // Calendar
1021
1043
  // ================================================================
1022
- function updateTodayDate(inputDate: Date) {
1044
+ function updateTodayDate(inputDate: Date, prevDate?: Date | null) {
1045
+ const previous = prevDate ?? prevDateRef.current;
1046
+ const hasMonthOrYearChanged =
1047
+ !previous ||
1048
+ previous.getMonth() !== inputDate.getMonth() ||
1049
+ previous.getFullYear() !== inputDate.getFullYear();
1050
+
1051
+ // Always sync the active day value
1023
1052
  setDay(inputDate.getDate());
1024
- setMonth(inputDate.getMonth());
1025
- setYear(inputDate.getFullYear());
1026
- setStartDay(getStartDayOfMonth(inputDate));
1027
1053
 
1028
- // update selector
1029
- setSelectedMonth(inputDate.getMonth());
1030
- setSelectedYear(inputDate.getFullYear());
1054
+ if (hasMonthOrYearChanged) {
1055
+ // Only update month/year related state when it actually changes;
1056
+ // this prevents expensive table re-inits for simple day clicks.
1057
+ setMonth(inputDate.getMonth());
1058
+ setYear(inputDate.getFullYear());
1059
+ setStartDay(getStartDayOfMonth(inputDate));
1031
1060
 
1032
-
1033
- setTimeout(() => {
1034
- // initialize table grid
1035
- tableGridInit();
1061
+ // update selector
1062
+ setSelectedMonth(inputDate.getMonth());
1063
+ setSelectedYear(inputDate.getFullYear());
1036
1064
 
1037
- // The scrollbar position is horizontal
1038
- bodyScrollbarInit();
1039
- }, 500);
1065
+ setTimeout(() => {
1066
+ // initialize table grid
1067
+ tableGridInit();
1040
1068
 
1069
+ // The scrollbar position is horizontal
1070
+ bodyScrollbarInit();
1071
+ }, 500);
1072
+ }
1073
+
1074
+ prevDateRef.current = inputDate;
1041
1075
  }
1042
1076
 
1043
1077
 
@@ -1136,6 +1170,29 @@ const EventCalendarTimeline = (props: EventCalendarTimelineProps) => {
1136
1170
 
1137
1171
  }
1138
1172
  function handleDayChange(e: React.MouseEvent, currentDay: number) {
1173
+ // Avoid triggering a full table re-render when the clicked cell is already
1174
+ // the active day; this becomes expensive with large datasets.
1175
+ if (
1176
+ date.getDate() === currentDay &&
1177
+ date.getMonth() === month &&
1178
+ date.getFullYear() === year
1179
+ ) {
1180
+ return;
1181
+ }
1182
+
1183
+ // !!! Important, performance optimization for large data renderings
1184
+ /*
1185
+ - Previously, every time I clicked on the date, 'setDate' would be called in 'useEffect([date])',
1186
+ and 'tableGridInit + bodyScrollbarInit' would be run regardless of whether it was across months/years,
1187
+ and these recalculations and DOM operations were very time-consuming when 1000+ pieces of data, causing lag.
1188
+
1189
+ - Now remember the last date with 'prevDateRef', reset the month/year related state only when the month or year
1190
+ changes and execute 'tableGridInit/scrollbarInit'. Only 'day' is updated when the date is changed, avoiding
1191
+ the relayout of the entire table and the initialization of the scrollbar.
1192
+
1193
+ -'handleDayChange' still updates 'date', but since the month/year has not changed, it no longer triggers heavy
1194
+ initialization, significantly reducing stuttering when clicking, while the month/year switch triggers a full recount as usual.
1195
+ */
1139
1196
  setDate(new Date(year, month, currentDay));
1140
1197
  }
1141
1198
 
@@ -1866,30 +1923,44 @@ const EventCalendarTimeline = (props: EventCalendarTimelineProps) => {
1866
1923
 
1867
1924
 
1868
1925
  function tableGridInitHeadertitle() {
1926
+
1869
1927
  //
1870
1928
  if (tableGridRef.current === null) return;
1871
1929
 
1872
1930
  const tableGridEl: any = tableGridRef.current;
1873
1931
 
1874
- // initialize cell height
1875
- const headerTitleTbody = tableGridEl.querySelector('.custom-event-tl-table__datagrid-body__title tbody');
1876
- const contentTbody = tableGridEl.querySelector('.custom-event-tl-table__datagrid-body__content tbody');
1877
- if (!headerTitleTbody || !contentTbody) return;
1932
+ // Use requestAnimationFrame to batch DOM operations
1933
+ const rafId = requestAnimationFrame(() => {
1934
+
1878
1935
 
1879
- const headerTitleTrElements = headerTitleTbody.getElementsByTagName('tr');
1880
- const trElements = contentTbody.getElementsByTagName('tr');
1936
+ // initialize cell height
1937
+ const headerTitleTbody = tableGridEl.querySelector('.custom-event-tl-table__datagrid-body__title tbody');
1938
+ const contentTbody = tableGridEl.querySelector('.custom-event-tl-table__datagrid-body__content tbody');
1939
+ if (!headerTitleTbody || !contentTbody) return;
1940
+
1941
+ const headerTitleTrElements = headerTitleTbody.getElementsByTagName('tr');
1942
+ const trElements = contentTbody.getElementsByTagName('tr');
1943
+
1944
+ // Reset any previously set inline heights so we measure natural heights.
1945
+ for (let i = 0; i < headerTitleTrElements.length; i++) {
1946
+ // set to 'auto' (or remove inline style) to allow shrink
1947
+ headerTitleTrElements[i].style.height = 'auto';
1948
+ if (trElements[i]) trElements[i].style.height = 'auto';
1949
+
1950
+ const targetElement = headerTitleTrElements[i].offsetHeight > trElements[i].offsetHeight ? headerTitleTrElements[i] : trElements[i];
1951
+ const tdOHeight = window.getComputedStyle(targetElement).height;
1952
+ headerTitleTrElements[i].style.height = tdOHeight;
1953
+ trElements[i].style.height = tdOHeight;
1954
+ }
1955
+
1956
+
1957
+ // Remove from tracking set after execution
1958
+ rafIdsRef.current.delete(rafId);
1959
+ });
1960
+
1961
+ // Track this requestAnimationFrame ID for cleanup
1962
+ rafIdsRef.current.add(rafId);
1881
1963
 
1882
- // Reset any previously set inline heights so we measure natural heights.
1883
- for (let i = 0; i < headerTitleTrElements.length; i++) {
1884
- // set to 'auto' (or remove inline style) to allow shrink
1885
- headerTitleTrElements[i].style.height = 'auto';
1886
- if (trElements[i]) trElements[i].style.height = 'auto';
1887
-
1888
- const targetElement = headerTitleTrElements[i].offsetHeight > trElements[i].offsetHeight ? headerTitleTrElements[i] : trElements[i];
1889
- const tdOHeight = window.getComputedStyle(targetElement).height;
1890
- headerTitleTrElements[i].style.height = tdOHeight;
1891
- trElements[i].style.height = tdOHeight;
1892
- }
1893
1964
 
1894
1965
 
1895
1966
  }
@@ -1909,223 +1980,234 @@ const EventCalendarTimeline = (props: EventCalendarTimelineProps) => {
1909
1980
  _curColCount = 7;
1910
1981
  }
1911
1982
 
1912
- //****************
1913
- // STEP 1-1:
1914
- //****************
1915
- // calculate min width (MODE: WEEK)
1916
- //--------------
1917
- if (appearanceMode === 'week') {
1918
- const tableMaxWidth = tableGridEl.clientWidth;
1919
- const tableHeaderTitleWidth = tableGridEl.querySelector('.custom-event-tl-table__cell-cushion-headertitle').clientWidth;
1920
- const tableDividerWidth = tableGridEl.querySelector('.custom-event-tl-table__timeline-divider').clientWidth;
1921
- const tableBorderWidth = 4;
1922
- const scrollMaxWidth = tableMaxWidth - tableHeaderTitleWidth - tableDividerWidth - tableBorderWidth;
1983
+ // Use requestAnimationFrame to batch DOM operations
1984
+ const rafId = requestAnimationFrame(() => {
1985
+
1923
1986
 
1924
- _curCellMinWidth = scrollMaxWidth / 7;
1925
- _curColCount = 7;
1987
+ //****************
1988
+ // STEP 1-1:
1989
+ //****************
1990
+ // calculate min width (MODE: WEEK)
1991
+ //--------------
1992
+ if (appearanceMode === 'week') {
1993
+ const tableMaxWidth = tableGridEl.clientWidth;
1994
+ const tableHeaderTitleWidth = tableGridEl.querySelector('.custom-event-tl-table__cell-cushion-headertitle').clientWidth;
1995
+ const tableDividerWidth = tableGridEl.querySelector('.custom-event-tl-table__timeline-divider').clientWidth;
1996
+ const tableBorderWidth = 4;
1997
+ const scrollMaxWidth = tableMaxWidth - tableHeaderTitleWidth - tableDividerWidth - tableBorderWidth;
1926
1998
 
1927
- // header
1928
- tableGridEl.querySelectorAll('.custom-event-tl-table__cell-cushion-headercontent__container, .custom-event-tl-table__cell-cushion-content').forEach((node: HTMLDivElement) => {
1929
- node.style.width = _curCellMinWidth + 'px';
1930
- });
1999
+ _curCellMinWidth = scrollMaxWidth / 7;
2000
+ _curColCount = 7;
1931
2001
 
2002
+ // header
2003
+ tableGridEl.querySelectorAll('.custom-event-tl-table__cell-cushion-headercontent__container, .custom-event-tl-table__cell-cushion-content').forEach((node: HTMLDivElement) => {
2004
+ node.style.width = _curCellMinWidth + 'px';
2005
+ });
1932
2006
 
1933
- }
1934
2007
 
2008
+ }
1935
2009
 
1936
2010
 
1937
- //****************
1938
- // STEP 1-2:
1939
- //****************
1940
- // calculate min width (MODE: MONTH)
1941
- //--------------
1942
- const cellMinWidth = _curCellMinWidth;
1943
- const colCount = _curColCount;
1944
- const scrollableMinWidth = cellMinWidth * colCount;
1945
2011
 
2012
+ //****************
2013
+ // STEP 1-2:
2014
+ //****************
2015
+ // calculate min width (MODE: MONTH)
2016
+ //--------------
2017
+ const cellMinWidth = _curCellMinWidth;
2018
+ const colCount = _curColCount;
2019
+ const scrollableMinWidth = cellMinWidth * colCount;
1946
2020
 
1947
- //****************
1948
- // STEP 1-3:
1949
- //****************
1950
- // initialize "header & main" cells
1951
- //--------------
1952
- const headerThContentContainers: any = tableGridEl.querySelector('.custom-event-tl-table__datagrid-header__content tbody').getElementsByTagName('th');
1953
- for (let i = 0; i < headerThContentContainers.length; i++) {
1954
- const curHeaderThContent = headerThContentContainers[i].querySelector('.custom-event-tl-table__cell-cushion-headercontent');
1955
- if (curHeaderThContent !== null) curHeaderThContent.style.width = _curCellMinWidth + 'px';
1956
- }
1957
2021
 
2022
+ //****************
2023
+ // STEP 1-3:
2024
+ //****************
2025
+ // initialize "header & main" cells
2026
+ //--------------
2027
+ const headerThContentContainers: any = tableGridEl.querySelector('.custom-event-tl-table__datagrid-header__content tbody').getElementsByTagName('th');
2028
+ for (let i = 0; i < headerThContentContainers.length; i++) {
2029
+ const curHeaderThContent = headerThContentContainers[i].querySelector('.custom-event-tl-table__cell-cushion-headercontent');
2030
+ if (curHeaderThContent !== null) curHeaderThContent.style.width = _curCellMinWidth + 'px';
2031
+ }
1958
2032
 
1959
- const mainTdContentContainers: any = tableGridEl.querySelector('.custom-event-tl-table__datagrid-body__content tbody').getElementsByTagName('td');
1960
- for (let i = 0; i < mainTdContentContainers.length; i++) {
1961
- const curHeaderThContent = mainTdContentContainers[i].querySelector('.custom-event-tl-table__cell-cushion-content');
1962
- if (curHeaderThContent !== null) curHeaderThContent.style.width = _curCellMinWidth + 'px';
1963
- }
1964
2033
 
1965
- const mainTdContentCols: any = tableGridEl.querySelector('.custom-event-tl-table__datagrid-body__content colgroup').getElementsByTagName('col')
1966
- for (let i = 0; i < mainTdContentCols.length; i++) {
1967
- mainTdContentCols[i].style.minWidth = _curCellMinWidth + 'px';
1968
- }
2034
+ const mainTdContentContainers: any = tableGridEl.querySelector('.custom-event-tl-table__datagrid-body__content tbody').getElementsByTagName('td');
2035
+ for (let i = 0; i < mainTdContentContainers.length; i++) {
2036
+ const curHeaderThContent = mainTdContentContainers[i].querySelector('.custom-event-tl-table__cell-cushion-content');
2037
+ if (curHeaderThContent !== null) curHeaderThContent.style.width = _curCellMinWidth + 'px';
2038
+ }
1969
2039
 
2040
+ const mainTdContentCols: any = tableGridEl.querySelector('.custom-event-tl-table__datagrid-body__content colgroup').getElementsByTagName('col')
2041
+ for (let i = 0; i < mainTdContentCols.length; i++) {
2042
+ mainTdContentCols[i].style.minWidth = _curCellMinWidth + 'px';
2043
+ }
1970
2044
 
1971
- //****************
1972
- // STEP 2:
1973
- //****************
1974
- // initialize scrollable wrapper (width)
1975
- //--------------
1976
- const _scrollableWrapper: HTMLElement[] = tableGridEl.querySelectorAll('.custom-event-tl-table__scroller-harness');
1977
- [].slice.call(_scrollableWrapper).forEach((el: any) => {
1978
- const scrollType = el.dataset.scroll;
1979
2045
 
1980
- if (appearanceMode === 'week') {
1981
- el.classList.add('custom-event-tl-table__scroller-harness--hideX');
1982
- }
1983
- if (appearanceMode === 'month') {
1984
- el.classList.remove('custom-event-tl-table__scroller-harness--hideX');
1985
- }
2046
+ //****************
2047
+ // STEP 2:
2048
+ //****************
2049
+ // initialize scrollable wrapper (width)
2050
+ //--------------
2051
+ const _scrollableWrapper: HTMLElement[] = tableGridEl.querySelectorAll('.custom-event-tl-table__scroller-harness');
2052
+ [].slice.call(_scrollableWrapper).forEach((el: any) => {
2053
+ const scrollType = el.dataset.scroll;
1986
2054
 
1987
- if (scrollType !== 'list') {
1988
- const _content = el.querySelector('.custom-event-tl-table__scroller');
1989
- const tableMaxWidth = tableGridEl.clientWidth;
1990
- const tableHeaderTitleWidth = tableGridEl.querySelector('.custom-event-tl-table__cell-cushion-headertitle').clientWidth;
1991
- const tableDividerWidth = tableGridEl.querySelector('.custom-event-tl-table__timeline-divider').clientWidth;
1992
- const tableBorderWidth = 4;
1993
- const scrollMaxWidth = tableMaxWidth - tableHeaderTitleWidth - tableDividerWidth - tableBorderWidth;
2055
+ if (appearanceMode === 'week') {
2056
+ el.classList.add('custom-event-tl-table__scroller-harness--hideX');
2057
+ }
2058
+ if (appearanceMode === 'month') {
2059
+ el.classList.remove('custom-event-tl-table__scroller-harness--hideX');
2060
+ }
1994
2061
 
1995
- el.dataset.width = scrollMaxWidth;
1996
- el.style.maxWidth = el.dataset.width + 'px';
1997
- _content.style.minWidth = scrollableMinWidth + 'px';
2062
+ if (scrollType !== 'list') {
2063
+ const _content = el.querySelector('.custom-event-tl-table__scroller');
2064
+ const tableMaxWidth = tableGridEl.clientWidth;
2065
+ const tableHeaderTitleWidth = tableGridEl.querySelector('.custom-event-tl-table__cell-cushion-headertitle').clientWidth;
2066
+ const tableDividerWidth = tableGridEl.querySelector('.custom-event-tl-table__timeline-divider').clientWidth;
2067
+ const tableBorderWidth = 4;
2068
+ const scrollMaxWidth = tableMaxWidth - tableHeaderTitleWidth - tableDividerWidth - tableBorderWidth;
1998
2069
 
1999
- }
2000
- });
2070
+ el.dataset.width = scrollMaxWidth;
2071
+ el.style.maxWidth = el.dataset.width + 'px';
2072
+ _content.style.minWidth = scrollableMinWidth + 'px';
2073
+
2074
+ }
2075
+ });
2001
2076
 
2002
2077
 
2003
- //****************
2004
- // STEP 3:
2005
- //****************
2006
- // initialize cell width
2007
- //--------------
2008
- const tdElementMaxWidth: number = typeof mainTdContentContainers[0] === 'undefined' ? 0 : parseFloat(window.getComputedStyle(mainTdContentContainers[0].querySelector('.custom-event-tl-table__cell-cushion-content')).maxWidth);
2078
+ //****************
2079
+ // STEP 3:
2080
+ //****************
2081
+ // initialize cell width
2082
+ //--------------
2083
+ const tdElementMaxWidth: number = typeof mainTdContentContainers[0] === 'undefined' ? 0 : parseFloat(window.getComputedStyle(mainTdContentContainers[0].querySelector('.custom-event-tl-table__cell-cushion-content')).maxWidth);
2009
2084
 
2010
2085
 
2011
- if (Array.isArray(eventsValue) && eventsValue.length > 0) {
2086
+ if (Array.isArray(eventsValue) && eventsValue.length > 0) {
2012
2087
 
2013
- for (let i = 0; i < headerThContentContainers.length; i++) {
2088
+ for (let i = 0; i < headerThContentContainers.length; i++) {
2014
2089
 
2015
- const curHeaderThContent = headerThContentContainers[i].querySelector('.custom-event-tl-table__cell-cushion-headercontent');
2016
- const curHeaderThContentMaxWidth = parseFloat(window.getComputedStyle(curHeaderThContent).width);
2017
- const targetElement = headerThContentContainers[i].offsetWidth > mainTdContentContainers[i].offsetWidth ? headerThContentContainers[i] : mainTdContentContainers[i];
2018
- let tdOwidth = parseFloat(window.getComputedStyle(targetElement).width);
2090
+ const curHeaderThContent = headerThContentContainers[i].querySelector('.custom-event-tl-table__cell-cushion-headercontent');
2091
+ const curHeaderThContentMaxWidth = parseFloat(window.getComputedStyle(curHeaderThContent).width);
2092
+ const targetElement = headerThContentContainers[i].offsetWidth > mainTdContentContainers[i].offsetWidth ? headerThContentContainers[i] : mainTdContentContainers[i];
2093
+ let tdOwidth = parseFloat(window.getComputedStyle(targetElement).width);
2019
2094
 
2020
2095
 
2021
- // check td max width
2022
- if (tdElementMaxWidth > 0 && tdOwidth > tdElementMaxWidth) {
2023
- tdOwidth = tdElementMaxWidth;
2024
- }
2096
+ // check td max width
2097
+ if (tdElementMaxWidth > 0 && tdOwidth > tdElementMaxWidth) {
2098
+ tdOwidth = tdElementMaxWidth;
2099
+ }
2025
2100
 
2026
- // check header th max width
2027
- if (tdElementMaxWidth > 0 && tdElementMaxWidth < curHeaderThContentMaxWidth) {
2028
- tdOwidth = curHeaderThContentMaxWidth;
2029
- }
2101
+ // check header th max width
2102
+ if (tdElementMaxWidth > 0 && tdElementMaxWidth < curHeaderThContentMaxWidth) {
2103
+ tdOwidth = curHeaderThContentMaxWidth;
2104
+ }
2030
2105
 
2031
- // Prevent the width from being +1 each time it is initialized
2032
- tdOwidth = tdOwidth - 1;
2106
+ // Prevent the width from being +1 each time it is initialized
2107
+ tdOwidth = tdOwidth - 1;
2033
2108
 
2034
2109
 
2035
- headerThContentContainers[i].querySelector('.custom-event-tl-table__cell-cushion-headercontent').style.width = tdOwidth + 'px';
2036
- mainTdContentCols[i].style.minWidth = tdOwidth + 'px';
2110
+ headerThContentContainers[i].querySelector('.custom-event-tl-table__cell-cushion-headercontent').style.width = tdOwidth + 'px';
2111
+ mainTdContentCols[i].style.minWidth = tdOwidth + 'px';
2037
2112
 
2038
2113
 
2114
+ }
2039
2115
  }
2040
- }
2041
2116
 
2042
2117
 
2043
- //****************
2044
- // STEP 4:
2045
- //****************
2046
- // initialize max width of table content
2047
- //--------------
2048
- if (scrollBodyRef.current !== null && scrollHeaderRef.current !== null) {
2049
- const tableContentWidth = window.getComputedStyle(tableGridEl.querySelector('.custom-event-tl-table__datagrid-body__content')).width;
2050
- const scrollBodyEl: any = scrollBodyRef.current;
2051
- const scrollHeaderEl: any = scrollHeaderRef.current;
2118
+ //****************
2119
+ // STEP 4:
2120
+ //****************
2121
+ // initialize max width of table content
2122
+ //--------------
2123
+ if (scrollBodyRef.current !== null && scrollHeaderRef.current !== null) {
2124
+ const tableContentWidth = window.getComputedStyle(tableGridEl.querySelector('.custom-event-tl-table__datagrid-body__content')).width;
2125
+ const scrollBodyEl: any = scrollBodyRef.current;
2126
+ const scrollHeaderEl: any = scrollHeaderRef.current;
2052
2127
 
2053
- scrollBodyEl.style.width = tableContentWidth;
2054
- scrollHeaderEl.style.width = tableContentWidth;
2055
- scrollBodyEl.dataset.width = parseFloat(tableContentWidth);
2056
- scrollHeaderEl.dataset.width = parseFloat(tableContentWidth);
2128
+ scrollBodyEl.style.width = tableContentWidth;
2129
+ scrollHeaderEl.style.width = tableContentWidth;
2130
+ scrollBodyEl.dataset.width = parseFloat(tableContentWidth);
2131
+ scrollHeaderEl.dataset.width = parseFloat(tableContentWidth);
2057
2132
 
2058
- //
2059
- const tableWrapperMaxWidthLatest = tableGridEl.clientWidth;
2060
- if (tableWrapperMaxWidthLatest > parseFloat(tableContentWidth)) {
2061
- tableGridEl.querySelector('.custom-event-tl-table__timeline-table').style.width = tableContentWidth;
2062
- }
2133
+ //
2134
+ const tableWrapperMaxWidthLatest = tableGridEl.clientWidth;
2135
+ if (tableWrapperMaxWidthLatest > parseFloat(tableContentWidth)) {
2136
+ tableGridEl.querySelector('.custom-event-tl-table__timeline-table').style.width = tableContentWidth;
2137
+ }
2063
2138
 
2064
2139
 
2065
- }
2140
+ }
2066
2141
 
2067
2142
 
2068
2143
 
2069
- //****************
2070
- // STEP 5:
2071
- //****************
2072
- // initialize cell height
2073
- //--------------
2074
- tableGridInitHeadertitle();
2144
+ //****************
2145
+ // STEP 5:
2146
+ //****************
2147
+ // initialize cell height
2148
+ //--------------
2149
+ tableGridInitHeadertitle();
2075
2150
 
2076
- //****************
2077
- // STEP 6:
2078
- //****************
2079
- //initialize scrollable wrapper (height)
2080
- //--------------
2081
- [].slice.call(_scrollableWrapper).forEach((el: any) => {
2082
- const scrollType = el.dataset.scroll;
2083
- const oldHeight = el.clientHeight;
2151
+ //****************
2152
+ // STEP 6:
2153
+ //****************
2154
+ //initialize scrollable wrapper (height)
2155
+ //--------------
2156
+ [].slice.call(_scrollableWrapper).forEach((el: any) => {
2157
+ const scrollType = el.dataset.scroll;
2158
+ const oldHeight = el.clientHeight;
2084
2159
 
2085
- if (scrollType !== 'header') {
2086
- const tableWrapperMaxHeight = window.getComputedStyle(tableGridEl as HTMLElement).height;
2087
- if (oldHeight > parseFloat(tableWrapperMaxHeight)) {
2088
- el.style.height = tableWrapperMaxHeight;
2160
+ if (scrollType !== 'header') {
2161
+ const tableWrapperMaxHeight = window.getComputedStyle(tableGridEl as HTMLElement).height;
2162
+ if (oldHeight > parseFloat(tableWrapperMaxHeight)) {
2163
+ el.style.height = tableWrapperMaxHeight;
2164
+ }
2089
2165
  }
2090
- }
2091
2166
 
2092
- });
2167
+ });
2093
2168
 
2094
2169
 
2095
2170
 
2096
- //****************
2097
- // STEP 7:
2098
- //****************
2099
- // display wrapper
2100
- //--------------
2101
- tableGridEl.classList.remove('invisible');
2171
+ //****************
2172
+ // STEP 7:
2173
+ //****************
2174
+ // display wrapper
2175
+ //--------------
2176
+ setGridReady(true);
2102
2177
 
2103
- //****************
2104
- // STEP 1-1:
2105
- //****************
2106
- // calculate min width (MODE: WEEK)
2107
- //--------------
2108
- if (appearanceMode === 'week') {
2109
- const tableMaxWidth = tableGridEl.clientWidth;
2110
- const tableHeaderTitleWidth = tableGridEl.querySelector('.custom-event-tl-table__cell-cushion-headertitle').clientWidth;
2111
- const tableDividerWidth = tableGridEl.querySelector('.custom-event-tl-table__timeline-divider').clientWidth;
2112
- const tableBorderWidth = 4;
2113
- const scrollMaxWidth = tableMaxWidth - tableHeaderTitleWidth - tableDividerWidth - tableBorderWidth;
2178
+ //****************
2179
+ // STEP 7-1:
2180
+ //****************
2181
+ // calculate min width (MODE: WEEK)
2182
+ //--------------
2183
+ if (appearanceMode === 'week') {
2184
+ const tableMaxWidth = tableGridEl.clientWidth;
2185
+ const tableHeaderTitleWidth = tableGridEl.querySelector('.custom-event-tl-table__cell-cushion-headertitle').clientWidth;
2186
+ const tableDividerWidth = tableGridEl.querySelector('.custom-event-tl-table__timeline-divider').clientWidth;
2187
+ const tableBorderWidth = 4;
2188
+ const scrollMaxWidth = tableMaxWidth - tableHeaderTitleWidth - tableDividerWidth - tableBorderWidth;
2114
2189
 
2115
- _curCellMinWidth = scrollMaxWidth / 7;
2116
- _curColCount = 7;
2190
+ _curCellMinWidth = scrollMaxWidth / 7;
2191
+ _curColCount = 7;
2117
2192
 
2118
- // header content
2119
- tableGridEl.querySelectorAll('.custom-event-tl-table__cell-cushion-headercontent__container, .custom-event-tl-table__cell-cushion-headercontent').forEach((node: HTMLDivElement) => {
2120
- node.style.width = _curCellMinWidth + 'px';
2121
- });
2193
+ // header content
2194
+ tableGridEl.querySelectorAll('.custom-event-tl-table__cell-cushion-headercontent__container, .custom-event-tl-table__cell-cushion-headercontent').forEach((node: HTMLDivElement) => {
2195
+ node.style.width = _curCellMinWidth + 'px';
2196
+ });
2122
2197
 
2123
- // main content
2124
- tableGridEl.querySelectorAll('.custom-event-tl-table__cell-cushion-content').forEach((node: HTMLDivElement) => {
2125
- node.style.width = _curCellMinWidth + 'px';
2126
- });
2198
+ // main content
2199
+ tableGridEl.querySelectorAll('.custom-event-tl-table__cell-cushion-content').forEach((node: HTMLDivElement) => {
2200
+ node.style.width = _curCellMinWidth + 'px';
2201
+ });
2127
2202
 
2128
- }
2203
+ }
2204
+
2205
+ // Remove from tracking set after execution
2206
+ rafIdsRef.current.delete(rafId);
2207
+ });
2208
+
2209
+ // Track this requestAnimationFrame ID for cleanup
2210
+ rafIdsRef.current.add(rafId);
2129
2211
 
2130
2212
  }
2131
2213
 
@@ -2160,6 +2242,52 @@ const EventCalendarTimeline = (props: EventCalendarTimelineProps) => {
2160
2242
 
2161
2243
  }
2162
2244
 
2245
+ // Dual RAF survival test
2246
+ /*
2247
+ A single RAF only guarantees "moving to the next frame."
2248
+ Only dual RAFs can guarantee "DOM is mounted + layout is stable."
2249
+ */
2250
+ const [gridReady, setGridReady] = useState<boolean>(false);
2251
+ const rafRef = useRef<{ r1?: number; r2?: number }>({});
2252
+
2253
+ function safeShowGrid() {
2254
+ //
2255
+ setGridReady(false);
2256
+
2257
+ rafRef.current.r1 = requestAnimationFrame(() => {
2258
+ rafRef.current.r2 = requestAnimationFrame(() => {
2259
+ const el = tableGridRef.current;
2260
+ if (!el) return;
2261
+
2262
+ //
2263
+ tableGridInit();
2264
+
2265
+ // Do only one thing: notify React that it’s ready to be displayed.
2266
+ setGridReady(true);
2267
+ });
2268
+ });
2269
+ }
2270
+
2271
+ useEffect(() => {
2272
+ safeShowGrid();
2273
+
2274
+ return () => {
2275
+ if (rafRef.current.r1) {
2276
+ cancelAnimationFrame(rafRef.current.r1);
2277
+ }
2278
+ if (rafRef.current.r2) {
2279
+ cancelAnimationFrame(rafRef.current.r2);
2280
+ }
2281
+ };
2282
+ }, [appearanceMode]);
2283
+
2284
+
2285
+ // Make sure the left side is highly synchronized with the right side.
2286
+ useEffect(() => {
2287
+ tableGridInitHeadertitle();
2288
+ }, [orginalData, appearanceMode]);
2289
+
2290
+
2163
2291
  // if user change the selectedYear, then udate the years array that is displayed on year tab view
2164
2292
  useEffect(() => {
2165
2293
  const years: number[] = [];
@@ -2172,7 +2300,7 @@ const EventCalendarTimeline = (props: EventCalendarTimelineProps) => {
2172
2300
 
2173
2301
 
2174
2302
  useEffect(() => {
2175
- updateTodayDate(date);
2303
+ updateTodayDate(date, prevDateRef.current);
2176
2304
  }, [date]);
2177
2305
 
2178
2306
 
@@ -2222,6 +2350,18 @@ const EventCalendarTimeline = (props: EventCalendarTimelineProps) => {
2222
2350
  //--------------
2223
2351
  return () => {
2224
2352
 
2353
+ // Cancel all pending requestAnimationFrame callbacks to prevent memory leaks
2354
+ rafIdsRef.current.forEach((rafId) => {
2355
+ cancelAnimationFrame(rafId);
2356
+ });
2357
+ rafIdsRef.current.clear();
2358
+
2359
+ // Cancel table drag animation frame if pending
2360
+ if (tableDragRafIdRef.current !== null) {
2361
+ cancelAnimationFrame(tableDragRafIdRef.current);
2362
+ tableDragRafIdRef.current = null;
2363
+ }
2364
+
2225
2365
  // reset table grid
2226
2366
  tableGridReset();
2227
2367
 
@@ -2475,8 +2615,11 @@ const EventCalendarTimeline = (props: EventCalendarTimelineProps) => {
2475
2615
  <div
2476
2616
  ref={tableGridRef}
2477
2617
  className={combinedCls(
2478
- `custom-event-tl-table__timeline-table__wrapper custom-event-tl-table__timeline-table__wrapper--${appearanceMode} invisible`,
2479
- tableWrapperClassName
2618
+ `custom-event-tl-table__timeline-table__wrapper custom-event-tl-table__timeline-table__wrapper--${appearanceMode}`,
2619
+ tableWrapperClassName,
2620
+ {
2621
+ 'invisible': !gridReady
2622
+ }
2480
2623
  )}
2481
2624
  onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => {
2482
2625
  onKeyPressed?.(e, selectedCells);