1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258open!Core_kernelopenPolyopen!ImportopenVdomincludeTable_intfletcutoffcomparex=Incr.set_cutoffx(Incr.Cutoff.of_comparecompare);x;;moduleMake(Row_id:Id)(Column_id:Id)(Sort_spec:Sort_spec)=structincludeUtilmoduleRow_id=Row_idmoduleColumn_id=Column_idmoduleSort_spec=Sort_specmoduleSort_key=Sort_spec.Sort_keymoduleSort_dir=Sort_spec.Sort_dirmoduleSort_criteria=structmoduleBy_column=structtype'at={column:'a;dir:Sort_dir.t}[@@derivingfields,compare,sexp]endtype'at='aBy_column.tlist[@@derivingcompare,sexp]letmap:'at->f:('a->'b)->'bt=funt~f->List.mapt~f:(fun(by_column:_By_column.t)->{by_columnwithcolumn=fby_column.column});;letfilter_map:'at->f:('a->'boption)->'bt=funt~f->List.filter_mapt~f:(fun(by_column:_By_column.t)->Option.map(fby_column.column)~f:(funcolumn->{by_columnwithcolumn}));;endmoduleBase_sort_criteria=structtypet=Column_id.tSort_criteria.By_column.tlist[@@derivingcompare,sexp]letnone=[]letfind_precedence_and_dirttarget=List.find_mapit~f:(funi{Sort_criteria.By_column.column;dir}->Option.some_if(Column_id.equalcolumntarget)(i+1,dir));;letremovettarget=List.filtert~f:(fun{Sort_criteria.By_column.column;dir=_}->not(Column_id.equalcolumntarget));;letadd_to_fronttcolumndir={Sort_criteria.By_column.column;dir}::tendmoduleColumn=structtype'at={header:Node.t;header_style:Css_gen.t;group:stringoption;sort_by:(Row_id.t->'a->Sort_key.t)option}[@@derivingfields]letcreate?group?sort_by?(header_style=Css_gen.empty)~header()={header;header_style;group;sort_by};;endmoduleRow_node_spec=Row_node_specmoduleVisibility_info=structtypet={tbody_rect:floatJs_misc.Rect.t;view_rect:floatJs_misc.Rect.t}[@@derivingcompare,sexp,fields]endmoduleKey=structmoduleT=structtypet={sort_criteria:Sort_key.toptionLazy.tSort_criteria.t;row_id:Row_id.t}[@@derivingsexp,fields]letsort_keyst=List.mapt.sort_criteria~f:Sort_criteria.By_column.columnletsort_dirst=List.mapt.sort_criteria~f:Sort_criteria.By_column.dir(** The comparison function here is written so that any two keys with the same
sort_dir sort according to that sort_dir; but keys with different sort_dirs just
have a stable relation between them. This allows us to have one Key type that is
used by all the different sorting situations, without needing to change
comparators. *)letcomparet1t2=matcht1.sort_criteria,t2.sort_criteriawith|[],[]->Row_id.comparet1.row_idt2.row_id|_::_,[]->-1|[],_::_->1|b::_,_::_->letmoduleB=Sort_criteria.By_columninletcompare_by_col=Comparable.lexicographic[(funb1b2->Sort_dir.compareb1.B.dirb2.B.dir);(funb1b2->matchforceb1.B.column,forceb2.B.columnwith|None,None->0|Some_,None->-1|None,Some_->1|Somek1,Somek2->Sort_spec.compare_keysb1.B.dirk1k2)]inletcompare_if_equal_keys=Sort_spec.compare_rows_if_equal_keys~cmp_row_id:Row_id.compareb.B.dirinComparable.lexicographic[(funt1t2->List.comparecompare_by_colt1.sort_criteriat2.sort_criteria);(funt1t2->compare_if_equal_keyst1.row_idt2.row_id)]t1t2;;letcreatesort_criteriarow_id={sort_criteria;row_id}endincludeTincludeComparable.Make(T)letconvert_sort_criteriasort_criteriarow_idrow=Sort_criteria.mapsort_criteria~f:(funcolumn->lazy(Option.map(Column.sort_bycolumn)~f:(funsort_by->sort_byrow_idrow)));;letsortsort_criteria~(rows:_Row_id.Map.tIncr.t)=letcreate_keyrow_iddata=create(convert_sort_criteriasort_criteriarow_iddata)row_idinIncr.Map.unordered_foldrows~init:Map.empty~add:(fun~key:row_id~dataacc->letkey=create_keyrow_iddatainMap.setacc~key~data)~remove:(fun~key:row_id~dataacc->letkey=create_keyrow_iddatainMap.removeacckey);;endmoduleHtml_id=structtypet=string[@@derivingcompare,sexp](* This module avoids using [sprintf] for performance reasons *)lettabletable_id="table-"^Table_id.to_stringtable_idlettbodytable_id=tabletable_id^"-body"lettheadtable_id=tabletable_id^"-header"letcolumn_grouptable_id=tabletable_id^"-column-group"letcolumn_headertable_id=tabletable_id^"-column-header"letcolumn_header_celltable_idcolumn_id=tabletable_id^"-header-cell-"^Column_id.to_stringcolumn_id;;letrowtable_idrow_id=tabletable_id^"-row-"^Row_id.to_stringrow_idletcell_of_partsrow_html_idcolumn_id_str=row_html_id^"-col-"^column_id_strletcelltable_idrow_idcolumn_id=cell_of_parts(rowtable_idrow_id)(Column_id.to_stringcolumn_id);;letspacerkey="spacer-"^keyendmoduleRow_view=Partial_render_list.Make(Row_id)(Key)moduleModel=structtypet={id:Table_id.t(* To avoid DOM id collisions. Never changes. *)(* Settings from client. Never change *);float_header:Float_type.t;float_first_col:Float_type.t;scroll_margin:Margin.t;scroll_region:Scroll_region.Id.t(* UI state. Changed by user during app usage *);focus_row:Row_id.toption;focus_col:Column_id.toption;sort_criteria:Base_sort_criteria.t(* Info measured from render. Changes each render. *);height_cache:Row_view.Height_cache.t;visibility_info:Visibility_info.toption;col_group_row_height:int;tbody_html_id:Html_id.t;thead_html_id:Html_id.t;column_group_html_id:Html_id.t;column_header_html_id:Html_id.t}[@@derivingfields,compare,sexp_of]letcreate~scroll_margin~scroll_region~float_header~float_first_col~height_guess?id?(initial_sort=Base_sort_criteria.none)?initial_focus_row?initial_focus_col()=letid=matchidwith|Someid->id|None->Table_id.create()in{id;float_header;float_first_col;scroll_margin;scroll_region;focus_row=initial_focus_row;focus_col=initial_focus_col;sort_criteria=initial_sort;height_cache=Row_view.Height_cache.empty~height_guess;visibility_info=None;col_group_row_height=0;tbody_html_id=Html_id.tbodyid;thead_html_id=Html_id.theadid;column_group_html_id=Html_id.column_groupid;column_header_html_id=Html_id.column_headerid};;letsort_dirst=List.mapt.sort_criteria~f:Sort_criteria.By_column.dirletsort_columnst=List.mapt.sort_criteria~f:Sort_criteria.By_column.columnletset_float_first_col=Field.fsetFields.float_first_colletset_float_header=Field.fsetFields.float_headerletset_scroll_margin=Field.fsetFields.scroll_marginletset_sort_criteria=Field.fsetFields.sort_criterialetcycle_sorting?keep_existing_colstcolumn_id~next_dir=letprev_dir=Base_sort_criteria.find_precedence_and_dirt.sort_criteriacolumn_id|>Option.map~f:sndinletcleared_sort_criteria=matchkeep_existing_colswith|None->Base_sort_criteria.none|Some()->Base_sort_criteria.removet.sort_criteriacolumn_idinletsort_criteria=matchnext_dirprev_dirwith|None->cleared_sort_criteria|Somedir->Base_sort_criteria.add_to_frontcleared_sort_criteriacolumn_iddirin{twithsort_criteria};;letget_tbody_rectt=Option.mapt.visibility_info~f:Visibility_info.tbody_rectendmoduleAction=structtypet=|Sort_column_clickedofColumn_id.t|Move_focus_rowofFocus_dir.t|Move_focus_colofFocus_dir.t|Set_focus_rowofRow_id.toption|Set_focus_colofColumn_id.toption|Page_focus_rowofFocus_dir.t[@@derivingsexp,compare,variants]endtype'arow_renderer=row_id:Row_id.t->row:'aIncr.t->Row_node_spec.tIncr.tletset_focus_row(m:Model.t)row_id=if[%compare.equal:Row_id.toption]m.focus_rowrow_idthenmelse{mwithfocus_row=row_id};;letset_focus_col(m:Model.t)col_id=if[%compare.equal:Column_id.toption]m.focus_colcol_idthenmelse{mwithfocus_col=col_id};;moduleExtra_model=structtype'at={rows:'aRow_id.Map.t;sorted_rows:'aKey.Map.t;columns:(Column_id.t*'aColumn.t)Int.Map.t;column_num_lookup:intColumn_id.Map.t;sort_criteria:'aColumn.tSort_criteria.t;row_view:'aRow_view.t;scroll_region:Scroll_region.toption;has_col_groups:bool;floating_col:Column_id.toption}[@@derivingfields]endmoduleExtra=structincludeExtra_modelletcreatem~rows~(columns:(Column_id.t*_Column.t)listIncr.t)=letscroll_region=(* This needs to fire whenever the model or rows change so that it can actually
find the element [scroll_region] after it is drawn. *)let%mapm=mand_=rowsinScroll_region.of_id(Model.scroll_regionm)inletsort_criteria=let%mapsort_criteria=m>>|Model.sort_criteria|>cutoff[%compare:Base_sort_criteria.t]andcolumns=columnsinSort_criteria.filter_mapsort_criteria~f:(funcolumn_id->List.find_mapcolumns~f:(fun(id,c)->if[%compare.equal:Column_id.t]idcolumn_idthenSomecelseNone))inletsorted_rows=sort_criteria>>=Key.sort~rowsinletmeasurements=let%mapvisibility_info=m>>|Model.visibility_infoinOption.mapvisibility_info~f:(fun{Visibility_info.tbody_rect;view_rect;_}->{Partial_render_list.Measurements.list_rect=tbody_rect;view_rect})inletfloating_col=let%mapfloat_first_col=m>>|Model.float_first_colandcolumns=columnsinifFloat_type.is_floatingfloat_first_colthenOption.map(List.hdcolumns)~f:fstelseNoneinletheight_cache=m>>|Model.height_cacheinletcolumn_num_lookup=let%mapcolumns=columnsinColumn_id.Map.of_alist_exn(List.mapicolumns~f:(funi(col_id,_)->col_id,i))inlethas_col_groups=let%mapcolumns=columnsinList.existscolumns~f:(fun(_,col)->Option.is_somecol.group)inletcolumns=let%mapcolumns=columnsinInt.Map.of_alist_exn(List.mapicolumns~f:(funicol->i,col))inlet%maprow_view=Row_view.create~rows:sorted_rows~height_cache~measurementsandrows=rowsandsorted_rows=sorted_rowsandcolumns=columnsandcolumn_num_lookup=column_num_lookupandsort_criteria=sort_criteriaandscroll_region=scroll_regionandfloating_col=floating_colandhas_col_groups=has_col_groupsin{rows;sorted_rows;columns;column_num_lookup;sort_criteria;row_view;scroll_region;has_col_groups;floating_col};;letcurrent_key(t:_t)~row_id=letopenOption.Let_syntaxinlet%maprow=Row_id.Map.findt.rowsrow_idinletsort_criteria=Key.convert_sort_criteriat.sort_criteriarow_idrowinKey.createsort_criteriarow_id;;letvisible_rowst=Row_view.rows_to_rendert.row_view|>Row_view.Sort_key.Map.keys|>List.map~f:Row_view.Sort_key.row_id;;letmove_focus_rowm(t:_t)~dir=letfocus_row=letopenOption.Let_syntaxinletfocus_key=let%bindrow_id=m.Model.focus_rowincurrent_keyt~row_idinlet%map{row_id;_},_=Util.move_focust.sorted_rowsfocus_keydirinrow_idinifOption.is_somefocus_rowthen{mwithfocus_row}elsem;;letmove_focus_colm(t:_t)~dir=letfocus_col=letopenOption.Let_syntaxinletfocus_key=let%bindcol_id=m.Model.focus_colinMap.findt.column_num_lookupcol_idinlet%map_,(col_id,_)=Util.move_focust.columnsfocus_keydirincol_idinifOption.is_somefocus_colthen{mwithfocus_col}elsem;;(* Possible offset due to floating header *)letget_top_margin_offset(m:Model.t)=letget_float_elem_size()=Option.map(Dom_html.getElementById_optm.thead_html_id)~f:(funel->Js_misc.viewport_rect_of_elementel|>Js_misc.Rect.float_height)inFloat_type.compute_offsetm.float_header~get_float_elem_size;;(* Possible offset due to floating first column *)letget_left_margin_offset(m:Model.t)(t:_t)~is_floating_col=ifis_floating_colthen0.else(letget_float_elem_size()=letopenOption.Let_syntaxinlet%bind_,(first_column_id,_)=Map.min_eltt.columnsinlet%mapel=Dom_html.getElementById_opt(Html_id.column_header_cellm.idfirst_column_id)inJs_misc.viewport_rect_of_elementel|>Js_misc.Rect.float_widthinFloat_type.compute_offsetm.float_first_col~get_float_elem_size);;letcall_row_scroll_functiond~row_id~f=Option.map(current_keyd~row_id)~f:(funkey->fd.row_view~key);;letis_floating_col(t:_t)column_id=[%compare.equal:Column_id.toption]t.floating_col(Somecolumn_id);;letcall_col_scroll_function?f_if_currently_floating(m:Model.t)~column_id~f~is_floating_col=letopenOption.Let_syntaxinlet%mapcell_rect=let%mapheader_cell=Dom_html.getElementById_opt(Html_id.column_header_cellm.idcolumn_id)inJs_misc.viewport_rect_of_elementheader_celland{tbody_rect;view_rect;_}=m.visibility_infoinletelem_start,elem_end,is_currently_floating=ifnotis_floating_colthenJs_misc.Rect.leftcell_rect,Js_misc.Rect.rightcell_rect,falseelse(letleft=Js_misc.Rect.lefttbody_rectinletwidth=Js_misc.Rect.float_widthcell_rectinletis_currently_floating=matchFloat_type.px_from_edgem.float_first_colwith|None->false|Somepx->letfloat_pos_left=view_rect.left+.Float.of_intpxinletfloat_pos_right=float_pos_left+.widthinFloat.(<=)tbody_rect.leftfloat_pos_left&&Float.(>=)tbody_rect.rightfloat_pos_rightinleft,left+.width,is_currently_floating)inletf=matchis_currently_floating,f_if_currently_floatingwith|true,Somef'->f'|false,_|_,None->finf~scroll_region_start:view_rect.left~scroll_region_end:view_rect.right~elem_start~elem_end;;letscroll_row_into_scroll_region(m:Model.t)(t:_t)row_id=lettop_margin_offset=get_top_margin_offsetminletf=Row_view.scroll_into_scroll_region?in_:t.scroll_region~top_margin:(m.scroll_margin.top+.top_margin_offset)~bottom_margin:m.scroll_margin.bottominOption.value(call_row_scroll_functiont~row_id~f)~default:`Didn't_scroll;;letscroll_col_into_scroll_region(m:Model.t)(t:_t)column_id=letis_floating_col=is_floating_coltcolumn_idinletleft_margin_offset=get_left_margin_offsetmt~is_floating_colinletf=Scroll.scroll_into_region?in_:t.scroll_regionHorizontal~start_margin:(m.scroll_margin.left+.left_margin_offset)~end_margin:m.scroll_margin.rightinletf_if_currently_floating~scroll_region_start:_~scroll_region_end:_~elem_start:_~elem_end:_=`Didn't_scrollinOption.value(call_col_scroll_functionm~column_id~f~f_if_currently_floating~is_floating_col)~default:`Didn't_scroll;;letscroll_row_to_position?keep_in_scroll_region(m:Model.t)(t:_t)row_id~position=letf=matchkeep_in_scroll_regionwith|None->Row_view.scroll_to_position?in_:t.scroll_region~position|Some()->lettop_margin_offset=get_top_margin_offsetminRow_view.scroll_to_position_and_into_region?in_:t.scroll_region~position~top_margin:(m.scroll_margin.top+.top_margin_offset)~bottom_margin:m.scroll_margin.bottominOption.value(call_row_scroll_functiont~row_id~f)~default:`Didn't_scroll;;letscroll_col_to_position?keep_in_scroll_region(m:Model.t)(t:_t)column_id~position=letis_floating_col=is_floating_coltcolumn_idinletscroll_to_position~scroll_region_start~scroll_region_end:_~elem_start~elem_end:_=Scroll.scroll_to_position?in_:t.scroll_regionHorizontal~position~scroll_region_start~elem_startinletscroll_to_position_and_into_region=letleft_margin_offset=get_left_margin_offsetmt~is_floating_colinScroll.scroll_to_position_and_into_region?in_:t.scroll_regionHorizontal~position~start_margin:(m.scroll_margin.left+.left_margin_offset)~end_margin:m.scroll_margin.rightinletf,f_if_currently_floating=matchkeep_in_scroll_regionwith|None->scroll_to_position,None|Some()->scroll_to_position_and_into_region,Somescroll_to_positioninOption.value(call_col_scroll_functionm~column_id~f~is_floating_col?f_if_currently_floating)~default:`Didn't_scroll;;letrow_is_in_scroll_region?scroll_margin(m:Model.t)(t:_t)row_id=lettop_margin_offset=get_top_margin_offsetminletf=letscroll_margin=Option.valuescroll_margin~default:m.scroll_margininRow_view.is_in_region~top_margin:(scroll_margin.top+.top_margin_offset)~bottom_margin:scroll_margin.bottominOption.join(call_row_scroll_functiont~row_id~f);;letcol_is_in_scroll_region?scroll_margin(m:Model.t)(t:_t)column_id=letis_floating_col=is_floating_coltcolumn_idinletleft_margin_offset=get_left_margin_offsetmt~is_floating_colinletf=letscroll_margin=Option.valuescroll_margin~default:m.scroll_margininScroll.is_in_region~start_margin:(scroll_margin.left+.left_margin_offset)~end_margin:scroll_margin.rightinletf_if_currently_floating~scroll_region_start:_~scroll_region_end:_~elem_start:_~elem_end:_=trueincall_col_scroll_functionm~column_id~f~f_if_currently_floating~is_floating_col;;letget_row_position(t:_t)row_id=letf=Row_view.get_positioninOption.join(call_row_scroll_functiont~row_id~f);;letget_col_position(m:Model.t)(t:_t)column_id=letis_floating_col=is_floating_coltcolumn_idinletf~scroll_region_start~scroll_region_end:_~elem_start~elem_end:_=Scroll.get_position~scroll_region_start~elem_startincall_col_scroll_functionm~column_id~f~is_floating_col;;letget_row_top_and_bottom(t:_t)row_id=letf=Row_view.get_top_and_bottominOption.join(call_row_scroll_functiont~row_id~f);;letget_col_left_and_right(m:Model.t)(t:_t)column_id=letis_floating_col=is_floating_coltcolumn_idinletf~scroll_region_start~scroll_region_end:_~elem_start~elem_end=letleft=Scroll.get_position~scroll_region_start~elem_startinleft,left+.elem_end-.elem_startincall_col_scroll_functionm~column_id~f~is_floating_col;;letscroll_focus_into_scroll_region(m:Model.t)d=letrow_scroll=Option.value_mapm.focus_row~default:`Didn't_scroll~f:(scroll_row_into_scroll_regionmd)inletcol_scroll=Option.value_mapm.focus_col~default:`Didn't_scroll~f:(scroll_col_into_scroll_regionmd)inScroll_result.combinerow_scrollcol_scroll;;letscroll_focus_to_position?keep_in_scroll_region(m:Model.t)d~position:(x,y)=letrow_scroll=Option.value_mapm.focus_row~default:`Didn't_scroll~f:(scroll_row_to_position?keep_in_scroll_regionmd~position:y)inletcol_scroll=Option.value_mapm.focus_col~default:`Didn't_scroll~f:(scroll_col_to_position?keep_in_scroll_regionmd~position:x)inScroll_result.combinerow_scrollcol_scroll;;letpage_focus_row_offset_and_focus_key(m:Model.t)(t:_t)~(dir:Focus_dir.t)=letopenOption.Let_syntaxinlet%bindvisibility_info=m.visibility_infoandfocus_row=m.focus_rowinlet%mapfocus_key=current_keyt~row_id:focus_rowinletfocus_height=Row_view.Height_cache.heightm.height_cache(Key.row_idfocus_key)inletscroll_height=Js_misc.Rect.float_heightvisibility_info.view_rectinlettop_margin_offset=get_top_margin_offsetminletmult=matchdirwith|Prev->-1.|Next->1.inletoffset=mult*.(scroll_height-.focus_height-.top_margin_offset)inoffset,focus_key;;letpage_focus_row(m:Model.t)(t:_t)~(dir:Focus_dir.t)=letopenOption.Let_syntaxinletnew_focus_row=let%bindoffset,focus_key=page_focus_row_offset_and_focus_keymt~dirinRow_view.find_by_relative_positiont.row_viewfocus_key~offsetinmatchnew_focus_rowwith|None->m|Somerow->set_focus_rowm(Some(Key.row_idrow));;letpage_focus_row_target_position(m:Model.t)(t:_t)~(dir:Focus_dir.t)=letopenOption.Let_syntaxinlet%mapmeasurements=Row_view.measurementst.row_viewandoffset,focus_key=page_focus_row_offset_and_focus_keymt~dirinlettop_of_table=measurements.list_rect.topinletpos_in_table=Row_view.focus_offset_to_positiont.row_viewfocus_key~offsetintop_of_table+.pos_in_table;;letfocus_is_in_scroll_region?scroll_margin(m:Model.t)(t:_t)=letrow=Option.bindm.focus_row~f:(row_is_in_scroll_region?scroll_marginmt)inletcol=Option.bindm.focus_col~f:(col_is_in_scroll_region?scroll_marginmt)inmatchrow,colwith|None,None->None|None,Someb|Someb,None->Someb|Someb1,Someb2->Some(b1&&b2);;letget_focus_position(m:Model.t)(t:_t)=(Option.bindm.focus_col~f:(get_col_positionmt),Option.bindm.focus_row~f:(get_row_positiont));;letget_focus_rect(m:Model.t)(t:_t)=letopenOption.Let_syntaxinlet%bindrow_id=m.focus_rowandcol_id=m.focus_colinlet%bindtop,bottom=get_row_top_and_bottomtrow_idinlet%mapleft,right=get_col_left_and_rightmtcol_idin{Js_misc.Rect.left;right;top;bottom};;letfind_row_by_position(m:Model.t)(t:_t)position=letopenOption.Let_syntaxinlet%map{Visibility_info.tbody_rect;_}=m.visibility_infoinletposition=position-.Js_misc.Rect.toptbody_rectinifFloat.is_negativepositionthen`Beforeelse(matchRow_view.find_by_positiont.row_view~positionwith|Some{Key.row_id;_}->`Atrow_id|None->`After);;letfind_col_by_position(m:Model.t)(t:_t)position=letopenOption.Let_syntaxinList.fold_until(Map.datat.columns)~init:true~f:(funis_first(col_id,_)->letcol_header_rect=lethtml_id=Html_id.column_header_cellm.idcol_idinlet%mapelem=Dom_html.getElementById_opthtml_idinJs_misc.viewport_rect_of_elementeleminmatchcol_header_rectwith|None->StopNone|Somerect->ifis_first&&position<Js_misc.Rect.leftrectthenStop(Some`Before)elseifposition<=Js_misc.Rect.rightrectthenStop(Some(`Atcol_id))elseContinuefalse)~finish:(function|false->Some`After|true->let%map{Visibility_info.tbody_rect;_}=m.visibility_infoinifFloat.(<)position(Js_misc.Rect.lefttbody_rect)then`Beforeelse`After);;(** returns the element associated with the row id in question *)letfind_row_elementtable_idrow_id=Dom_html.getElementById_opt(Html_id.rowtable_idrow_id);;letupdate_col_group_row_height(m:Model.t)(t:_t)=lethas_floating_header()=Float_type.is_floatingm.float_headerinletheight=ifnot(t.has_col_groups&&has_floating_header())thenNoneelseletopenOption.Let_syntaxinlet%mapcolumn_group=Dom_html.getElementById_opt(Html_id.column_groupm.id)andcolumn_header=Dom_html.getElementById_opt(Html_id.column_headerm.id)in(* We don't use [Js_misc.viewport_rect_of_element] here so that we can round down
instead of rounding to the nearest interger. This reduces jitter. *)letcolumn_group_top=column_group##getBoundingClientRect##.topinletcolumn_header_top=column_header##getBoundingClientRect##.topinint_of_float(column_header_top-.column_group_top)inOption.valueheight~default:0;;(** Computes and updates the heights of all rows that are currently rendered *)letupdate_height_cache(m:Model.t)(t:_t)=Row_view.measure_heightst.row_view~measure_row:(funkey->Option.map(find_row_elementm.idkey.row_id)~f:(funel->letrect=Js_misc.viewport_rect_of_elementelinJs_misc.Rect.toprect,Js_misc.Rect.bottomrect))~get_row_height:(fun~prev~curr~next->Option.mapcurr~f:(fun(curr_top,curr_bottom)->letwith_top_margin=Option.map(Option.mapprev~f:Tuple2.get2)~f:(funprev_bottom->curr_bottom-.prev_bottom)inletwith_bottom_margin=Option.map(Option.mapnext~f:Tuple2.get1)~f:(funnext_top->next_top-.curr_top)inmatchwith_top_margin,with_bottom_marginwith|Someh1,Someh2->(h1+.h2)/.2.|Someh,None|None,Someh->h|None,None->curr_bottom-.curr_top));;letupdate_visibility_info(m:Model.t)(t:_t)=letopenOption.Let_syntaxinletscroll_region=matcht.scroll_regionwith|Some_asa->a|None->Scroll_region.of_id(Model.scroll_regionm)inlet%mapscroll_region=scroll_regionandtbody=Dom_html.getElementById_optm.tbody_html_idinletview_rect=matchscroll_regionwith|Window->Js_misc.Rect.map(Js_misc.client_rect())~f:Float.of_int|Elementel->Js_misc.client_rect_of_elementelin{Visibility_info.tbody_rect=Js_misc.client_rect_of_elementtbody;view_rect};;endleton_display~(old_model:Model.toptionIncr.t)(model:Model.tIncr.t)extra=let%mapold_focus_row=old_model>>|Option.map~f:Model.focus_rowandfocus_row=model>>|Model.focus_rowandold_focus_col=old_model>>|Option.map~f:Model.focus_colandfocus_col=model>>|Model.focus_colandextra=extraandmodel=modelinfun()->ifold_focus_row<>Somefocus_row||old_focus_col<>Somefocus_colthen(letmaybe_scrollxf=Option.iterx~f:(funx->ignore(fextrax))inmaybe_scrollfocus_row(Extra.scroll_row_into_scroll_regionmodel);maybe_scrollfocus_col(Extra.scroll_col_into_scroll_regionmodel));;letsort_column_clicked=Model.cycle_sorting~next_dir:Sort_dir.nextletapply_actionmextra=letopenExtrainlet%mapm=mandextra=extrainfun(action:Action.t)->matchactionwith|Sort_column_clickedcolumn_id->sort_column_clickedmcolumn_id|Move_focus_rowdir->move_focus_rowmextra~dir|Move_focus_coldir->move_focus_colmextra~dir|Set_focus_rowrow_id->set_focus_rowmrow_id|Set_focus_colcol_id->set_focus_colmcol_id|Page_focus_rowdir->page_focus_rowmextra~dir;;letupdate_visibility(m:Model.tIncr.t)extra=let%mapm=mandextra=extrainfun~schedule_action:_->letvisibility_info=Extra.update_visibility_infomextrainletheight_cache=Extra.update_height_cachemextrainletcol_group_row_height=Extra.update_col_group_row_heightmextrainif[%compare.equal:Visibility_info.toption]visibility_infom.visibility_info&&[%compare.equal:Row_view.Height_cache.t]height_cachem.height_cache&&[%compare.equal:int]col_group_row_heightm.col_group_row_heightthenmelse{mwithvisibility_info;height_cache;col_group_row_height};;letspacer~key=letid_attr=Attr.id(Html_id.spacerkey)instage(funheight->[Node.tr~key[id_attr;Attr.style(Css_gen.height(`Px(Float.iround_nearest_exnheight)))][]]);;letsticky_pos(pos:Float_type.tIncr.t)=pos>>|Float_type.px_from_edgeletfinalize_sticky_possticky_pos=Option.mapsticky_pos~f:(funpos->`Pxpos)letsticky_style?left_sticky_pos?top_sticky_pos~z_index:z_ndx()=letsticky_style=matchleft_sticky_pos,top_sticky_poswith|None,None->Css_gen.empty|left,top->Css_gen.position?top?left`StickyinCss_gen.(z_indexz_ndx@>sticky_style);;letview_header?override_on_click~inject~columns~top_sticky_pos~left_sticky_posm=letget_sticky_style~top_sticky_pos=letfirst_cell=sticky_style?left_sticky_pos?top_sticky_pos~z_index:3()inletdefault=sticky_style?top_sticky_pos~z_index:2()infirst_cell,defaultinletget_sticky_attrs~top_sticky_pos=letfirst_cell,default=get_sticky_style~top_sticky_posinAttr.stylefirst_cell,Attr.styledefaultinletheader_nodes=let%mapsort_criteria=m>>|Model.sort_criteriaandid=m>>|Model.idandcolumns=columnsandtop_sticky_pos=matchtop_sticky_poswith|None->Incr.returnNone|Somepx->let%mapcol_group_row_height=m>>|Model.col_group_row_heightinfinalize_sticky_pos(Some(px+col_group_row_height))inletfirst_cell_sticky_style,default_sticky_style=get_sticky_style~top_sticky_posinList.mapi(Map.datacolumns)~f:(funi(key,data)->letsticky_style=ifi=0thenfirst_cell_sticky_styleelsedefault_sticky_styleinletprecedence_and_dir=Base_sort_criteria.find_precedence_and_dirsort_criteriakeyinletsort_direction_indicator=matchprecedence_and_dirwith|None->Node.none|Some(precedence,dir)->(matchSort_dir.indicatordir~precedencewith|None->Node.none|Someindicator->letindicator_attrs=Sort_dir.indicator_classdir~precedence|>Option.map~f:Attr.class_|>Option.to_listinNode.spanindicator_attrs[Node.text(sprintf" %s"indicator)])inletsort_direction_classes=matchprecedence_and_dirwith|None->[]|Some(precedence,dir)->List.filter_opt[Some"sorted";Sort_dir.header_classdir~precedence]inleton_click=Option.value_map(Column.sort_bydata)~default:[]~f:(fun_->[Attr.on_click(funev->matchoverride_on_clickwith|Someon_click->on_clickkeyev|None->inject(Action.Sort_column_clickedkey))])inletattrs=[Attr.id(Html_id.column_header_cellidkey);Attr.classes("column-header"::sort_direction_classes)]@on_click@[Attr.style(Css_gen.concat[sticky_style;data.Column.header_style])]inNode.thattrs[data.Column.header;sort_direction_indicator])inletgroup_nodes=lettop_sticky_pos=finalize_sticky_postop_sticky_posinletfirst_cell_sticky_attr,default_sticky_attr=get_sticky_attrs~top_sticky_posinlet%mapcolumns=columnsinletgroups=List.map(Map.datacolumns)~f:(func->(sndc).Column.group)inifList.for_allgroups~f:Option.is_nonethenNoneelse(letgrouped=List.groupigroups~break:(funixy->(i=1&&Option.is_someleft_sticky_pos)||not(Option.equalString.equalxy))inList.mapigrouped~f:(funil->letsticky_attr=ifi=0then[first_cell_sticky_attr]else[default_sticky_attr]inlettext,class_=matchOption.join(List.hdl)with|None->[],"column-group-empty"|Somes->[Node.texts],"column-group-full"inNode.th([Attr.classes["column-group";class_];Attr.create"colspan"(Int.to_string(List.lengthl))]@sticky_attr)text)|>Option.some)inletgroup_attrs=let%mapcolumn_group_html_id=m>>|Model.column_group_html_idin[Attr.idcolumn_group_html_id]inletheader_attrs=let%mapcolumn_header_html_id=m>>|Model.column_header_html_idin[Attr.idcolumn_header_html_id]inlet%mapgroup_nodes=group_nodesandheader_nodes=header_nodesandgroup_attrs=group_attrsandheader_attrs=header_attrsin[Option.mapgroup_nodes~f:(funn->Node.trgroup_attrsn);Some(Node.trheader_attrsheader_nodes)]|>List.filter_opt;;typerow_html_ids={row_html_id:Html_id.t;cell_html_ids:Html_id.tlist}letview_rendered_rows~table_id~column_ids~row_view~render_row~left_sticky_pos=letnon_sticky_style=sticky_style~z_index:1()inletsticky_style=sticky_style?left_sticky_pos~z_index:2()inlet%bindcolumn_ids=column_idsinletcolumn_id_strs=List.mapcolumn_ids~f:Column_id.to_stringin(* Annotate each row with its html ids - we do this because the string conversions can
be expensive, and don't need to be re-done every time a row's incremental fires. *)letrows_to_render_with_html_ids=Incr.Map.unordered_fold(row_view>>|Row_view.rows_to_render)~init:Key.Map.empty~add:(fun~key~data:rowacc->letrow_html_id=Html_id.rowtable_idkey.row_idinletcell_html_ids=List.mapcolumn_id_strs~f:(Html_id.cell_of_partsrow_html_id)inMap.setacc~key~data:({row_html_id;cell_html_ids},row))~remove:(fun~key~data:_acc->Map.removeacckey)~update:(fun~key~old_data:_~new_data:rowacc->Map.changeacckey~f:(Option.map~f:(Tuple2.map_snd~f:(fun_->row))))inIncr.Map.mapi'rows_to_render_with_html_ids~f:(fun~key~data->let%bind{row_html_id;cell_html_ids}=data>>|fstinlet%map{Row_node_spec.row_attrs;cells}=render_row~row_id:key.row_id~row:(data>>|snd)inletcells=List.zip_exncell_html_idscells|>List.mapi~f:(funi(cell_html_id,{Row_node_spec.Cell.attrs;nodes})->letsticky_style=ifi=0thensticky_styleelsenon_sticky_styleinletattrs=[Attr.stylesticky_style;Attr.idcell_html_id]@attrs|>Attrs.merge_classes_and_stylesinNode.tdattrsnodes)inNode.tr~key:row_html_id(row_attrs@[Attr.idrow_html_id])cells);;letview?override_header_on_clickmd~render_row~inject~attrs=letspacer_before=unstage(spacer~key:"before")inletspacer_after=unstage(spacer~key:"after")inletcolumns=d>>|Extra.columnsinletcolumn_ids=let%mapcolumn_ids=d>>|Extra.columnsinList.map(Map.datacolumn_ids)~f:fstinletrow_view=d>>|Extra.row_viewinlet%bindtable_id=m>>|Model.idandtop_sticky_pos=sticky_pos(m>>|Model.float_header)andleft_sticky_pos=sticky_pos(m>>|Model.float_first_col)inletleft_sticky_pos=finalize_sticky_posleft_sticky_posinlet%mapheader=view_header?override_on_click:override_header_on_click~inject~columns~top_sticky_pos~left_sticky_posmandrendered_rows=view_rendered_rows~table_id~column_ids~row_view~render_row~left_sticky_posandbefore_height,after_height=Row_view.spacer_heightsrow_viewinNode.tableattrs[Node.thead[Attr.id(Html_id.theadtable_id);Attr.style(Css_gen.background_color`Inherit)]header;Node.tbody[Attr.id(Html_id.tbodytable_id)](spacer_beforebefore_height@Map.datarendered_rows@spacer_afterafter_height)];;type'at=(Action.t,Model.t,unit,'aExtra.t)Component.with_extraletcreate?override_header_on_clickmodel~old_model~inject~rows~columns~render_row~attrs=letextra=Extra.createmodel~rows~columnsinlet%mapapply_action=let%mapapply_action=apply_actionmodelextrainfunaction_~schedule_action:_->apply_actionactionandon_display=let%mapon_display=on_display~old_modelmodelextrainfun_~schedule_action:_->on_display()andupdate_visibility=update_visibilitymodelextraandview=view?override_header_on_clickmodelextra~render_row~inject~attrsandextra=extraandmodel=modelinComponent.create_with_extra~on_display~update_visibility~apply_action~extramodelview;;leton_display~(old_model:Model.t)(m:Model.t)d=ifold_model.focus_row<>m.focus_row||old_model.focus_col<>m.focus_colthen(letmaybe_scrollxf=Option.iterx~f:(funx->ignore(fdx))inmaybe_scrollm.focus_row(Extra.scroll_row_into_scroll_regionm);maybe_scrollm.focus_col(Extra.scroll_col_into_scroll_regionm));;letapply_actionmd(action:Action.t)=matchactionwith|Sort_column_clickedcolumn_id->sort_column_clickedmcolumn_id|Move_focus_rowdir->Extra.move_focus_rowmd~dir|Move_focus_coldir->Extra.move_focus_colmd~dir|Set_focus_rowrow_id->set_focus_rowmrow_id|Set_focus_colcol_id->set_focus_colmcol_id|Page_focus_rowdir->Extra.page_focus_rowmd~dir;;letupdate_visibilitymd=letvisibility_info=Extra.update_visibility_infomdinletheight_cache=Extra.update_height_cachemdinletcol_group_row_height=Extra.update_col_group_row_heightmdinif[%compare.equal:Visibility_info.toption]visibility_infom.visibility_info&&[%compare.equal:Row_view.Height_cache.t]height_cachem.height_cache&&[%compare.equal:int]col_group_row_heightm.col_group_row_heightthenmelse{mwithvisibility_info;height_cache;col_group_row_height};;endmoduleDefault_sort_spec=structmoduleSort_key=structtypet=|Stringofstring|Floatoffloat|IntegerofInt63.t|Null[@@derivingcompare,sexp]endmoduleSort_dir=structtypet=|Ascending|Descending[@@derivingsexp,compare]letnext=function|None->SomeAscending|SomeAscending->SomeDescending|SomeDescending->None;;letindicatort~precedence=letdir_ind=matchtwith|Ascending->"▲"|Descending->"▼"inletprecedence_ind=matchprecedencewith|1->""|p->sprintf"(%d)"pinSome(dir_ind^precedence_ind);;letheader_classt~precedence:_=matchtwith|Ascending->Some"sorted-asc"|Descending->Some"sorted-desc";;letindicator_class_~precedence:_=Some"sorted-indicator"letsign=function|Ascending->1|Descending->-1;;endletcompare_keysdirk1k2=match(k1:Sort_key.t),(k2:Sort_key.t)with(* Always sort nulls last regardless of the sort direction *)|Null,_|_,Null->Sort_key.comparek1k2|_,_->Sort_key.comparek1k2*Sort_dir.signdir;;letcompare_rows_if_equal_keys~cmp_row_iddirr1r2=cmp_row_idr1r2*Sort_dir.signdir;;end