2026-02-15 13:13:18 +02:00
import { FilterLabelsByType } from "@triliumnext/commons" ;
import { Fragment , VNode } from "preact" ;
import { useContext } from "preact/hooks" ;
import Component from "../../components/component" ;
import FNote from "../../entities/fnote" ;
import NoteContextAwareWidget from "../note_context_aware_widget" ;
import { FormDropdownDivider , FormDropdownSubmenu , FormListItem , FormListToggleableItem } from "./FormList" ;
import FormTextBox from "./FormTextBox" ;
2026-02-15 17:30:39 +02:00
import { useNoteLabel , useNoteLabelBoolean } from "./hooks" ;
2026-02-15 13:13:18 +02:00
import { ParentComponent } from "./react_utils" ;
export interface ClickContext {
note : FNote ;
triggerCommand : NoteContextAwareWidget [ "triggerCommand" ] ;
}
export interface CheckBoxProperty {
type : "checkbox" ,
label : string ;
bindToLabel : FilterLabelsByType < boolean > ;
icon? : string ;
2026-02-15 17:30:39 +02:00
/** When true, the checkbox will be checked when the label value is false. Useful when the label represents a "hide" action, without exposing double negatives to the user. */
reverseValue? : boolean ;
2026-02-15 13:13:18 +02:00
}
export interface ButtonProperty {
type : "button" ,
label : string ;
title? : string ;
icon? : string ;
onClick ( context : ClickContext ) : void ;
}
export interface SplitButtonProperty extends Omit < ButtonProperty , "type" > {
type : "split-button" ;
items ( { note , parentComponent } : { note : FNote , parentComponent : Component } ) : VNode ;
}
export interface NumberProperty {
type : "number" ,
label : string ;
bindToLabel : FilterLabelsByType < number > ;
width? : number ;
min? : number ;
icon? : string ;
disabled ? : ( note : FNote ) = > boolean ;
}
export interface ComboBoxItem {
2026-02-15 13:26:23 +02:00
/ * *
* The value to set to the bound label , ` null ` has a special meaning which removes the label entirely .
* /
value : string | null ;
2026-02-15 13:13:18 +02:00
label : string ;
}
2026-02-15 14:20:09 +02:00
export interface ComboBoxGroup {
2026-02-15 13:13:18 +02:00
title : string ;
items : ComboBoxItem [ ] ;
}
2026-02-15 13:44:16 +02:00
interface Separator {
2026-02-15 13:21:29 +02:00
type : "separator"
}
2026-02-15 13:13:18 +02:00
export interface ComboBoxProperty {
type : "combobox" ,
label : string ;
icon? : string ;
bindToLabel : FilterLabelsByType < string > ;
/ * *
* The default value is used when the label is not set .
* /
defaultValue? : string ;
2026-02-15 13:44:16 +02:00
options : ( ComboBoxItem | Separator | ComboBoxGroup ) [ ] ;
2026-02-15 13:19:12 +02:00
dropStart? : boolean ;
2026-02-15 13:13:18 +02:00
}
2026-02-15 13:44:16 +02:00
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty | SplitButtonProperty | Separator ;
2026-02-15 13:13:18 +02:00
export function ViewProperty ( { note , property } : { note : FNote , property : BookProperty } ) {
switch ( property . type ) {
case "button" :
return < ButtonPropertyView note = { note } property = { property } / > ;
case "split-button" :
return < SplitButtonPropertyView note = { note } property = { property } / > ;
case "checkbox" :
return < CheckBoxPropertyView note = { note } property = { property } / > ;
case "number" :
return < NumberPropertyView note = { note } property = { property } / > ;
case "combobox" :
return < ComboBoxPropertyView note = { note } property = { property } / > ;
2026-02-15 13:44:16 +02:00
case "separator" :
return < FormDropdownDivider / > ;
2026-02-15 13:13:18 +02:00
}
}
function ButtonPropertyView ( { note , property } : { note : FNote , property : ButtonProperty } ) {
const parentComponent = useContext ( ParentComponent ) ;
return (
< FormListItem
icon = { property . icon }
title = { property . title }
onClick = { ( ) = > {
if ( ! parentComponent ) return ;
property . onClick ( {
note ,
triggerCommand : parentComponent.triggerCommand.bind ( parentComponent )
} ) ;
} }
> { property . label } < / FormListItem >
) ;
}
function SplitButtonPropertyView ( { note , property } : { note : FNote , property : SplitButtonProperty } ) {
const parentComponent = useContext ( ParentComponent ) ;
const ItemsComponent = property . items ;
const clickContext = parentComponent && {
note ,
triggerCommand : parentComponent.triggerCommand.bind ( parentComponent )
} ;
return ( parentComponent &&
< FormDropdownSubmenu
icon = { property . icon ? ? "bx bx-empty" }
title = { property . label }
onDropdownToggleClicked = { ( ) = > clickContext && property . onClick ( clickContext ) }
>
< ItemsComponent note = { note } parentComponent = { parentComponent } / >
< / FormDropdownSubmenu >
) ;
}
function NumberPropertyView ( { note , property } : { note : FNote , property : NumberProperty } ) {
//@ts-expect-error Interop with text box which takes in string values even for numbers.
const [ value , setValue ] = useNoteLabel ( note , property . bindToLabel ) ;
const disabled = property . disabled ? . ( note ) ;
return (
< FormListItem
icon = { property . icon }
disabled = { disabled }
onClick = { ( e ) = > e . stopPropagation ( ) }
>
{ property . label }
< FormTextBox
type = "number"
currentValue = { value ? ? "" } onChange = { setValue }
style = { { width : ( property . width ? ? 100 ) } }
min = { property . min ? ? 0 }
disabled = { disabled }
/ >
< / FormListItem >
) ;
}
function ComboBoxPropertyView ( { note , property } : { note : FNote , property : ComboBoxProperty } ) {
2026-02-15 13:34:48 +02:00
const [ value , setValue ] = useNoteLabel ( note , property . bindToLabel ) ;
const valueWithDefault = value ? ? property . defaultValue ? ? null ;
2026-02-15 13:13:18 +02:00
function renderItem ( option : ComboBoxItem ) {
return (
< FormListItem
key = { option . value }
2026-02-15 13:34:48 +02:00
checked = { valueWithDefault === option . value }
2026-02-15 13:13:18 +02:00
onClick = { ( ) = > setValue ( option . value ) }
>
{ option . label }
< / FormListItem >
) ;
}
return (
< FormDropdownSubmenu
title = { property . label }
icon = { property . icon ? ? "bx bx-empty" }
2026-02-15 13:19:12 +02:00
dropStart = { property . dropStart }
2026-02-15 13:13:18 +02:00
>
{ ( property . options ) . map ( ( option , index ) = > {
if ( "items" in option ) {
return (
< Fragment key = { option . title } >
< FormListItem key = { option . title } disabled > { option . title } < / FormListItem >
{ option . items . map ( renderItem ) }
{ index < property . options . length - 1 && < FormDropdownDivider / > }
< / Fragment >
) ;
}
2026-02-15 13:21:29 +02:00
if ( "type" in option ) {
return < FormDropdownDivider key = { index } / > ;
}
2026-02-15 13:13:18 +02:00
return renderItem ( option ) ;
} ) }
< / FormDropdownSubmenu >
) ;
}
function CheckBoxPropertyView ( { note , property } : { note : FNote , property : CheckBoxProperty } ) {
const [ value , setValue ] = useNoteLabelBoolean ( note , property . bindToLabel ) ;
return (
< FormListToggleableItem
icon = { property . icon }
title = { property . label }
2026-02-15 17:30:39 +02:00
currentValue = { property . reverseValue ? ! value : value }
onChange = { newValue = > setValue ( property . reverseValue ? ! newValue : newValue ) }
2026-02-15 13:13:18 +02:00
/ >
) ;
}