@@ -10,9 +10,9 @@ import {
1010 MenuItem ,
1111 Popover ,
1212 Position ,
13- Checkbox ,
1413 Button ,
1514 InputGroup ,
15+ Intent ,
1616} from "@blueprintjs/core" ;
1717import ReactDOM from "react-dom" ;
1818import getUids from "roamjs-components/dom/getUids" ;
@@ -56,15 +56,24 @@ const NodeSearchMenu = ({
5656 triggerPosition,
5757 triggerText,
5858} : { onClose : ( ) => void } & Props ) => {
59+ const MENU_WIDTH = 400 ;
5960 const [ activeIndex , setActiveIndex ] = useState ( 0 ) ;
6061 const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
61- const [ isFilterMenuOpen , setIsFilterMenuOpen ] = useState ( false ) ;
6262 const [ discourseTypes , setDiscourseTypes ] = useState < DiscourseNode [ ] > ( [ ] ) ;
6363 const [ checkedTypes , setCheckedTypes ] = useState < Record < string , boolean > > ( { } ) ;
6464 const [ isLoading , setIsLoading ] = useState ( true ) ;
6565 const [ searchResults , setSearchResults ] = useState < Record < string , Result [ ] > > (
6666 { } ,
6767 ) ;
68+ const [ isFilterMenuVisible , setIsFilterMenuVisible ] = useState ( false ) ;
69+ const typeIds = useMemo (
70+ ( ) => discourseTypes . map ( ( t ) => t . type ) ,
71+ [ discourseTypes ] ,
72+ ) ;
73+ const isAllSelected = useMemo (
74+ ( ) => typeIds . length > 0 && typeIds . every ( ( id ) => ! ! checkedTypes [ id ] ) ,
75+ [ typeIds , checkedTypes ] ,
76+ ) ;
6877 const scrollContainerRef = useRef < HTMLDivElement | null > ( null ) ;
6978 const searchTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
7079 const POPOVER_TOP_OFFSET = 30 ;
@@ -194,7 +203,7 @@ const NodeSearchMenu = ({
194203
195204 const onSelect = useCallback (
196205 ( item : Result ) => {
197- waitForBlock ( blockUid , textarea . value ) . then ( ( ) => {
206+ void waitForBlock ( blockUid , textarea . value ) . then ( ( ) => {
198207 onClose ( ) ;
199208
200209 setTimeout ( ( ) => {
@@ -352,46 +361,69 @@ const NodeSearchMenu = ({
352361
353362 let currentGlobalIndex = - 1 ;
354363
355- const handleTypeCheckChange = useCallback (
356- ( typeKey : string , e : React . MouseEvent ) => {
357- e . preventDefault ( ) ;
358- e . stopPropagation ( ) ;
359-
360- setCheckedTypes ( ( prev ) => ( {
361- ...prev ,
362- [ typeKey ] : ! prev [ typeKey ] ,
363- } ) ) ;
364+ const handleTypeCheckChange = useCallback ( ( typeKey : string ) => {
365+ setCheckedTypes ( ( prev ) => ( {
366+ ...prev ,
367+ [ typeKey ] : ! prev [ typeKey ] ,
368+ } ) ) ;
369+ } , [ ] ) ;
364370
365- setTimeout ( ( ) => {
366- textarea . focus ( ) ;
367- const cursorPos = textarea . selectionStart ;
368- textarea . setSelectionRange ( cursorPos , cursorPos ) ;
369- } , 0 ) ;
371+ const handleToggleAll = useCallback (
372+ ( checked : boolean ) => {
373+ setCheckedTypes ( Object . fromEntries ( typeIds . map ( ( id ) => [ id , checked ] ) ) ) ;
370374 } ,
371- [ textarea ] ,
375+ [ typeIds ] ,
372376 ) ;
373377
374- const remainFocusOnTextarea = useCallback ( ( e : React . MouseEvent ) => {
375- e . preventDefault ( ) ;
376- e . stopPropagation ( ) ;
377- } , [ ] ) ;
378-
379- const toggleFilterMenu = useCallback (
380- ( e : React . MouseEvent ) => {
381- e . preventDefault ( ) ;
382- e . stopPropagation ( ) ;
383-
384- setIsFilterMenuOpen ( ( prev ) => ! prev ) ;
378+ const handleSelectOnly = useCallback (
379+ ( node : DiscourseNode ) => {
380+ const next = Object . fromEntries (
381+ typeIds . map ( ( id ) => [ id , id === node . type ] ) ,
382+ ) ;
383+ setCheckedTypes ( next as Record < string , boolean > ) ;
384+ } ,
385+ [ typeIds ] ,
386+ ) ;
385387
386- setTimeout ( ( ) => {
387- if ( textarea ) {
388- textarea . focus ( ) ;
389- const cursorPos = textarea . selectionStart ;
390- textarea . setSelectionRange ( cursorPos , cursorPos ) ;
391- }
392- } , 0 ) ;
388+ const renderTypeItem = useCallback (
389+ ( item : DiscourseNode ) => {
390+ const isSelected = ! ! checkedTypes [ item . type ] ;
391+ return (
392+ < MenuItem
393+ key = { item . type }
394+ className = "group !p-0"
395+ text = {
396+ < div className = "flex w-full items-center justify-between" >
397+ < div className = "flex flex-1 items-center px-2 py-1.5" >
398+ < span className = "mr-2" > { isSelected ? "✓" : " " } </ span >
399+ < span > { item . text } </ span >
400+ </ div >
401+ < Button
402+ minimal
403+ small
404+ className = "flex !h-full items-center justify-center !rounded-none px-3 opacity-0 transition-opacity group-hover:opacity-100"
405+ onClick = { ( e ) => {
406+ e . stopPropagation ( ) ;
407+ handleSelectOnly ( item ) ;
408+ } }
409+ onMouseDown = { ( e ) => e . preventDefault ( ) }
410+ >
411+ Only
412+ </ Button >
413+ </ div >
414+ }
415+ icon = { null }
416+ shouldDismissPopover = { false }
417+ onClick = { ( e ) => {
418+ e . preventDefault ( ) ;
419+ e . stopPropagation ( ) ;
420+ handleTypeCheckChange ( item . type ) ;
421+ } }
422+ onMouseDown = { ( e ) => e . preventDefault ( ) }
423+ />
424+ ) ;
393425 } ,
394- [ textarea ] ,
426+ [ checkedTypes , handleTypeCheckChange , handleSelectOnly ] ,
395427 ) ;
396428
397429 return (
@@ -400,11 +432,12 @@ const NodeSearchMenu = ({
400432 isOpen = { true }
401433 canEscapeKeyClose
402434 minimal
435+ usePortal = { true }
403436 target = { < span /> }
404437 position = { Position . BOTTOM_LEFT }
405438 modifiers = { {
406439 flip : { enabled : true } ,
407- preventOverflow : { enabled : true } ,
440+ preventOverflow : { enabled : true , boundariesElement : "viewport" } ,
408441 offset : {
409442 enabled : true ,
410443 fn : ( data ) => {
@@ -419,53 +452,54 @@ const NodeSearchMenu = ({
419452 content = {
420453 < div
421454 className = "discourse-node-search-menu"
422- style = { { width : "250px" } }
423- onMouseDown = { remainFocusOnTextarea }
424- onClick = { remainFocusOnTextarea }
455+ style = { { width : MENU_WIDTH } }
425456 >
426457 { isLoading ? (
427458 < div className = "p-3 text-center text-gray-500" > Loading...</ div >
428459 ) : (
429460 < >
430461 < div
431462 className = "discourse-node-search-menu"
432- style = { { width : "250px" } }
433- onMouseDown = { remainFocusOnTextarea }
434- onClick = { remainFocusOnTextarea }
463+ style = { { width : MENU_WIDTH } }
435464 >
436465 < div className = "flex items-center justify-between border-b border-gray-200 p-2" >
437- < div className = "text-sm font-semibold" > Search Results</ div >
438466 < Button
439467 icon = "filter"
440468 minimal
441469 small
442- active = { isFilterMenuOpen }
443- onClick = { toggleFilterMenu }
444- onMouseDown = { remainFocusOnTextarea }
445470 title = "Filter by type"
471+ onClick = { ( e ) => {
472+ e . stopPropagation ( ) ;
473+ e . preventDefault ( ) ;
474+ setIsFilterMenuVisible ( ! isFilterMenuVisible ) ;
475+ } }
476+ onMouseDown = { ( e ) => e . preventDefault ( ) }
446477 />
447478 </ div >
448-
449- { isFilterMenuOpen && (
450- < div className = "border-b border-gray-200 p-2" >
451- < div className = "mb-2 text-sm font-semibold" >
452- Filter by type:
479+ { isFilterMenuVisible && (
480+ < div >
481+ < div className = "flex items-center justify-between px-3 py-2" >
482+ < span className = "text-sm font-medium text-gray-700" >
483+ Filter by Type
484+ </ span >
485+ < Button
486+ small
487+ intent = { isAllSelected ? Intent . SUCCESS : Intent . PRIMARY }
488+ icon = { isAllSelected ? "tick" : "multi-select" }
489+ onMouseDown = { ( e ) => e . preventDefault ( ) }
490+ onClick = { ( e ) => {
491+ e . stopPropagation ( ) ;
492+ e . preventDefault ( ) ;
493+ handleToggleAll ( ! isAllSelected ) ;
494+ } }
495+ >
496+ { isAllSelected ? "All selected" : "Select all" }
497+ </ Button >
453498 </ div >
454- < div className = "flex flex-wrap gap-2" >
455- { discourseTypes . map ( ( type ) => (
456- < div
457- key = { type . type }
458- className = "inline-flex cursor-pointer items-center"
459- onClick = { ( e ) => handleTypeCheckChange ( type . type , e ) }
460- >
461- < Checkbox
462- label = { type . text }
463- checked = { checkedTypes [ type . type ] }
464- onChange = { ( ) => { } }
465- className = "m-0"
466- />
467- </ div >
468- ) ) }
499+ < div className = "max-h-48 overflow-y-auto" >
500+ < Menu >
501+ { discourseTypes . map ( ( t ) => renderTypeItem ( t ) ) }
502+ </ Menu >
469503 </ div >
470504 </ div >
471505 ) }
@@ -484,8 +518,10 @@ const NodeSearchMenu = ({
484518 < MenuItem
485519 key = { item . uid }
486520 text = { item . text }
521+ multiline
487522 data-active = { isActive }
488523 active = { isActive }
524+ shouldDismissPopover = { false }
489525 onClick = { ( ) => onSelect ( item ) }
490526 />
491527 ) ;
0 commit comments