@@ -99,12 +99,6 @@ const ResultHeader = React.forwardRef<
9999 ( ) => activeSort . findIndex ( ( s ) => s . key === c . key ) ,
100100 [ c . key , activeSort ] ,
101101 ) ;
102- const refCallback = useCallback (
103- ( r : HTMLTableDataCellElement ) => {
104- if ( ref && "current" in ref && ref . current ) ref . current [ c . uid ] = r ;
105- } ,
106- [ ref , c . uid ] ,
107- ) ;
108102 return (
109103 < td
110104 style = { {
@@ -113,7 +107,6 @@ const ResultHeader = React.forwardRef<
113107 width : columnWidth ,
114108 } }
115109 data-column = { c . uid }
116- ref = { refCallback }
117110 key = { c . uid }
118111 onClick = { ( ) => {
119112 if ( sortIndex >= 0 ) {
@@ -194,28 +187,29 @@ export const CellEmbed = ({
194187 ) ;
195188} ;
196189
197- type OnWidthUpdate = ( args : {
198- column : string ;
199- width : string ;
200- save ?: boolean ;
201- } ) => void ;
190+ type ResultRowProps = {
191+ r : Result ;
192+ columns : Column [ ] ;
193+ onDragStart : ( e : React . DragEvent < HTMLDivElement > ) => void ;
194+ onDrag : ( e : React . DragEvent < HTMLDivElement > ) => void ;
195+ onDragEnd : ( e : React . DragEvent < HTMLDivElement > ) => void ;
196+ parentUid : string ;
197+ ctrlClick ?: ( e : Result ) => void ;
198+ views : { column : string ; mode : string ; value : string } [ ] ;
199+ onRefresh : ( ) => void ;
200+ } ;
201+
202202const ResultRow = ( {
203203 r,
204+ columns,
204205 parentUid,
205206 ctrlClick,
206207 views,
208+ onDragStart,
209+ onDrag,
210+ onDragEnd,
207211 onRefresh,
208- columns,
209- onWidthUpdate,
210- } : {
211- r : Result ;
212- parentUid : string ;
213- ctrlClick ?: ( e : Result ) => void ;
214- views : { column : string ; mode : string ; value : string } [ ] ;
215- columns : Column [ ] ;
216- onRefresh : ( ) => void ;
217- onWidthUpdate : OnWidthUpdate ;
218- } ) => {
212+ } : ResultRowProps ) => {
219213 const cell = ( key : string ) => {
220214 const value = toCellValue ( {
221215 value : r [ `${ key } -display` ] || r [ key ] || "" ,
@@ -264,32 +258,6 @@ const ResultRow = ({
264258 [ views ] ,
265259 ) ;
266260 const trRef = useRef < HTMLTableRowElement > ( null ) ;
267- const dragHandler = useCallback (
268- ( e : React . DragEvent < HTMLDivElement > ) => {
269- const delta = e . clientX - e . currentTarget . getBoundingClientRect ( ) . left ;
270- const cellWidth =
271- e . currentTarget . parentElement ?. getBoundingClientRect ( ) . width ;
272- if ( typeof cellWidth === "undefined" ) return ;
273- if ( cellWidth + delta <= 0 ) return ;
274- const rowWidth =
275- e . currentTarget . parentElement ?. parentElement ?. getBoundingClientRect ( )
276- . width ;
277- if ( typeof rowWidth === "undefined" ) return ;
278- if ( cellWidth + delta >= rowWidth ) return ;
279- const column = e . currentTarget . getAttribute ( "data-column" ) ;
280- const save = e . type === "dragend" ;
281- if ( trRef . current ) {
282- trRef . current . style . cursor = save ? "" : "ew-resize" ;
283- }
284- if ( column )
285- onWidthUpdate ( {
286- column,
287- width : `${ ( ( cellWidth + delta ) / rowWidth ) * 100 } %` ,
288- save,
289- } ) ;
290- } ,
291- [ onWidthUpdate ] ,
292- ) ;
293261 return (
294262 < >
295263 < tr ref = { trRef } data-uid = { r . uid } >
@@ -350,21 +318,25 @@ const ResultRow = ({
350318 { i < columns . length - 1 && (
351319 < div
352320 style = { {
353- width : 1 ,
321+ width : 2 ,
354322 cursor : "ew-resize" ,
355323 position : "absolute" ,
356324 top : 0 ,
357325 right : 0 ,
358326 bottom : 0 ,
359327 background : `rgba(16,22,26,0.15)` ,
360328 } }
329+ data-left-column-uid = { columnUid }
330+ data-right-column-uid = { columns [ i + 1 ] . uid }
361331 data-column = { columnUid }
362332 draggable
363- onDragStart = { ( e ) =>
364- e . dataTransfer . setDragImage ( dragImage , 0 , 0 )
365- }
366- onDrag = { dragHandler }
367- onDragEnd = { dragHandler }
333+ onDragStart = { ( e ) => {
334+ e . dataTransfer . setData ( "text/plain" , "" ) ;
335+ e . dataTransfer . setDragImage ( dragImage , 0 , 0 ) ;
336+ onDragStart ( e ) ;
337+ } }
338+ onDrag = { onDrag }
339+ onDragEnd = { onDragEnd }
368340 />
369341 ) }
370342 </ td >
@@ -375,6 +347,18 @@ const ResultRow = ({
375347 ) ;
376348} ;
377349
350+ type ColumnWidths = {
351+ [ key : string ] : string ;
352+ } ;
353+
354+ type DragInfo = {
355+ startX : number ;
356+ leftColumnUid : string | null ;
357+ rightColumnUid : string | null ;
358+ leftStartWidth : number ;
359+ rightStartWidth : number ;
360+ } ;
361+
378362const ResultsTable = ( {
379363 columns,
380364 results,
@@ -417,42 +401,166 @@ const ResultsTable = ({
417401 allResults : Result [ ] ;
418402 showInterface ?: boolean ;
419403} ) => {
420- const columnWidths = useMemo ( ( ) => {
404+ const tableRef = useRef < HTMLTableElement | null > ( null ) ;
405+ const dragInfo = useRef < DragInfo > ( {
406+ startX : 0 ,
407+ leftColumnUid : null ,
408+ rightColumnUid : null ,
409+ leftStartWidth : 0 ,
410+ rightStartWidth : 0 ,
411+ } ) ;
412+
413+ const rafIdRef = useRef < number | null > ( null ) ;
414+ const throttledSetColumnWidths = useCallback ( ( update : ColumnWidths ) => {
415+ if ( rafIdRef . current !== null ) {
416+ cancelAnimationFrame ( rafIdRef . current ) ;
417+ }
418+ rafIdRef . current = requestAnimationFrame ( ( ) =>
419+ setColumnWidths (
420+ ( prev ) =>
421+ ( {
422+ ...prev ,
423+ ...update ,
424+ } ) as ColumnWidths ,
425+ ) ,
426+ ) ;
427+ } , [ ] ) ;
428+
429+ useEffect ( ( ) => {
430+ return ( ) => {
431+ if ( rafIdRef . current !== null ) {
432+ cancelAnimationFrame ( rafIdRef . current ) ;
433+ }
434+ } ;
435+ } , [ ] ) ;
436+
437+ const [ columnWidths , setColumnWidths ] = useState ( ( ) => {
421438 const widths =
422439 typeof layout . widths === "string" ? [ layout . widths ] : layout . widths || [ ] ;
423- return Object . fromEntries (
424- widths
425- . map ( ( w ) => {
426- const match = / ^ ( .* ) - ( [ ^ - ] + ) $ / . exec ( w ) ;
427- return match ;
428- } )
429- . filter ( ( m ) : m is RegExpExecArray => ! ! m )
430- . map ( ( match ) => {
431- return [ match [ 1 ] , match [ 2 ] ] ;
432- } ) ,
440+ const fromLayout = Object . fromEntries (
441+ widths . map ( ( w ) => w . split ( " - " ) ) . filter ( ( p ) => p . length === 2 ) ,
442+ ) ;
443+ const allWidths : ColumnWidths = { } ;
444+ const defaultWidth = `${ 100 / columns . length } %` ;
445+ columns . forEach ( ( c ) => {
446+ allWidths [ c . uid ] = fromLayout [ c . uid ] || defaultWidth ;
447+ } ) ;
448+ return allWidths ;
449+ } ) ;
450+
451+ const onDragStart = useCallback ( ( e ) => {
452+ const { leftColumnUid, rightColumnUid } = e . currentTarget . dataset ;
453+ if ( ! leftColumnUid || ! rightColumnUid || ! tableRef . current ) return ;
454+
455+ const leftHeader = tableRef . current ?. querySelector (
456+ `thead td[data-column="${ leftColumnUid } "]` ,
433457 ) ;
434- } , [ layout ] ) ;
435- const thRefs = useRef < Record < string , HTMLTableCellElement > > ( { } ) ;
436- const onWidthUpdate = useCallback < OnWidthUpdate > (
437- ( args ) => {
438- const cell = thRefs . current [ args . column ] ;
439- if ( ! cell ) return ;
440- cell . style . width = args . width ;
441- if ( args . save ) {
442- const layoutUid = getSubTree ( { parentUid, key : "layout" } ) . uid ;
443- if ( layoutUid )
444- setInputSettings ( {
445- blockUid : layoutUid ,
446- key : "widths" ,
447- values : Object . entries ( thRefs . current )
448- . map ( ( [ k , v ] ) => [ k , v . style . width ] )
449- . filter ( ( [ k , v ] ) => ! ! k && ! ! v )
450- . map ( ( [ k , v ] ) => `${ k } - ${ v } ` ) ,
451- } ) ;
458+ const rightHeader = tableRef . current ?. querySelector (
459+ `thead td[data-column="${ rightColumnUid } "]` ,
460+ ) ;
461+
462+ if ( ! leftHeader || ! rightHeader ) return ;
463+
464+ dragInfo . current = {
465+ startX : e . clientX ,
466+ leftColumnUid,
467+ rightColumnUid,
468+ leftStartWidth : ( leftHeader as HTMLElement ) . offsetWidth ,
469+ rightStartWidth : ( rightHeader as HTMLElement ) . offsetWidth ,
470+ } ;
471+ } , [ ] ) ;
472+
473+ const onDrag = useCallback ( ( e : React . DragEvent < HTMLDivElement > ) => {
474+ if ( e . clientX === 0 ) return ;
475+
476+ const {
477+ startX,
478+ leftColumnUid,
479+ rightColumnUid,
480+ leftStartWidth,
481+ rightStartWidth,
482+ } = dragInfo . current ;
483+
484+ if ( ! leftColumnUid || ! rightColumnUid ) return ;
485+
486+ const delta = e . clientX - startX ;
487+ const minWidth = 40 ;
488+
489+ let newLeftWidth = leftStartWidth + delta ;
490+ let newRightWidth = rightStartWidth - delta ;
491+
492+ const leftBelow = newLeftWidth < minWidth ;
493+ const rightBelow = newRightWidth < minWidth ;
494+
495+ if ( leftBelow && ! rightBelow ) {
496+ const adjustment = minWidth - newLeftWidth ;
497+ newLeftWidth = minWidth ;
498+ newRightWidth -= adjustment ;
499+ } else if ( rightBelow && ! leftBelow ) {
500+ const adjustment = minWidth - newRightWidth ;
501+ newRightWidth = minWidth ;
502+ newLeftWidth -= adjustment ;
503+ } else if ( leftBelow && rightBelow ) {
504+ const totalMin = minWidth * 2 ;
505+ const startTotal = leftStartWidth + rightStartWidth ;
506+
507+ if ( startTotal > totalMin ) {
508+ const scale = totalMin / startTotal ;
509+ newLeftWidth = Math . max ( minWidth , leftStartWidth * scale ) ;
510+ newRightWidth = Math . max ( minWidth , rightStartWidth * scale ) ;
511+ } else {
512+ newLeftWidth = leftStartWidth ;
513+ newRightWidth = rightStartWidth ;
452514 }
453- } ,
454- [ thRefs , parentUid ] ,
455- ) ;
515+ }
516+
517+ throttledSetColumnWidths ( {
518+ [ leftColumnUid ] : `${ newLeftWidth } px` ,
519+ [ rightColumnUid ] : `${ newRightWidth } px` ,
520+ } ) ;
521+ } , [ ] ) ;
522+
523+ const onDragEnd = useCallback ( ( ) => {
524+ if ( rafIdRef . current !== null ) {
525+ cancelAnimationFrame ( rafIdRef . current ) ;
526+ rafIdRef . current = null ;
527+ }
528+
529+ const totalWidth = tableRef . current ?. offsetWidth ;
530+ if ( ! totalWidth || totalWidth === 0 ) {
531+ return ;
532+ }
533+ const minWidth = 40 ;
534+ const minPercent = ( minWidth / totalWidth ) * 100 ;
535+
536+ const finalWidths : ColumnWidths = { } ;
537+ const uids = columns . map ( ( c ) => c . uid ) ;
538+ uids . forEach ( ( uid ) => {
539+ const header = tableRef . current ?. querySelector (
540+ `thead td[data-column="${ uid } "]` ,
541+ ) ;
542+ if ( header ) {
543+ const headerWidth = ( header as HTMLElement ) . offsetWidth ;
544+ if ( headerWidth > 0 ) {
545+ const percent = ( headerWidth / totalWidth ) * 100 ;
546+ finalWidths [ uid ] = `${ Math . max ( minPercent , percent ) } %` ;
547+ } else {
548+ finalWidths [ uid ] = columnWidths [ uid ] || "5%" ;
549+ }
550+ }
551+ } ) ;
552+ setColumnWidths ( finalWidths ) ;
553+
554+ const layoutUid = getSubTree ( { parentUid, key : "layout" } ) . uid ;
555+ if ( layoutUid ) {
556+ setInputSettings ( {
557+ blockUid : layoutUid ,
558+ key : "widths" ,
559+ values : Object . entries ( finalWidths ) . map ( ( [ k , v ] ) => `${ k } - ${ v } ` ) ,
560+ } ) ;
561+ }
562+ } , [ columns , parentUid , columnWidths ] ) ;
563+
456564 const resultHeaderSetFilters = React . useCallback (
457565 ( fs : FilterData ) => {
458566 setFilters ( fs ) ;
@@ -538,6 +646,7 @@ const ResultsTable = ({
538646 } , [ extraRowType , setExtraRowUid ] ) ;
539647 return (
540648 < HTMLTable
649+ elementRef = { tableRef }
541650 style = { {
542651 maxHeight : "400px" ,
543652 overflowY : "scroll" ,
@@ -554,7 +663,6 @@ const ResultsTable = ({
554663 < ResultHeader
555664 key = { c . uid }
556665 c = { c }
557- ref = { thRefs }
558666 allResults = { allResults }
559667 activeSort = { activeSort }
560668 setActiveSort = { setActiveSort }
@@ -575,7 +683,9 @@ const ResultsTable = ({
575683 views = { views }
576684 onRefresh = { onRefresh }
577685 columns = { columns }
578- onWidthUpdate = { onWidthUpdate }
686+ onDragStart = { onDragStart }
687+ onDrag = { onDrag }
688+ onDragEnd = { onDragEnd }
579689 />
580690 { extraRowUid === r . uid && (
581691 < tr className = { `roamjs-${ extraRowType } -row roamjs-extra-row` } >
0 commit comments