import { ReadOnlyReference , Reference } from "model/ref" ; import { Later , parseLaters , Time } from "model/time" ; import { AbstractTextComponent , Setting } from "obsidian" ; import { ReminderFormatType , ReminderFormatTypes } from "./format" ; class SettingRegistry { private settingContexts : Array < SettingContext > = [ ] ; register ( settingContext ) { this . settingContexts . push ( settingContext ) ; } findByKey ( key ) { return this . settingContexts . find ( c => c . key === key ) ; } forEach ( consumer ) { this . settingContexts . forEach ( consumer ) ; } } class SettingContext { private validationEl ? : HTMLElement ; private infoEl ? : HTMLElement ; private _setting ? : Setting ; public key ? : string ; public name ? : string ; public desc ? : string ; public tags : Array < string > = [ ] ; public settingModel ? : SettingModel < any , any > ; anyValueChanged ? : AnyValueChanged ; constructor ( private _settingRegistry ) { } init ( settingModel , setting , containerEl ) { this . settingModel = settingModel ; this . _setting = setting ; this . validationEl = containerEl . createDiv ( "validation" , el => { el . style . color = 'var(--text-error)' ; el . style . marginBottom = '1rem' ; el . style . fontSize = '14px' ; el . style . display = 'none' ; } ) ; this . infoEl = containerEl . createDiv ( "info" , el => { el . style . color = 'var(--text-faint)' ; el . style . marginBottom = '1rem' ; el . style . fontSize = '14px' ; el . style . display = 'none' ; } ) ; } setValidationError ( error ) { this . setText ( this . validationEl ! , error ) ; } setInfo ( info ) { this . setText ( this . infoEl ! , info ) ; } private setText ( el , text ) { if ( ! el ) { console . error ( "element not created" ) ; return ; } if ( text === null ) { el . style . display = "none" ; } else { el . style . display = "block" ; el . innerHTML = text ; } } get setting ( ) { return this . _setting ; } get registry ( ) { return this . _settingRegistry ; } hasTag ( tag ) { return this . tags . filter ( t => t === tag ) . length > 0 ; } update ( ) { if ( ! this . anyValueChanged ) { return ; } this . anyValueChanged ( this ) ; } setEnabled ( enable ) { this . setting ! . setDisabled ( ! enable ) ; } findContextByKey ( key ) { return this . _settingRegistry . findByKey ( key ) ; } booleanValue ( ) { return this . settingModel ! . value as boolean ; } isInitialized ( ) { return this . settingModel && this . validationEl && this . setting ; } } export class SettingModelBuilder { context : SettingContext ; constructor ( public registry ) { this . context = new SettingContext ( this . registry ) ; this . registry . register ( this . context ) ; } key ( key ) { this . context . key = key ; return this ; } name ( name ) { this . context . name = name ; return this ; } desc ( desc ) { this . context . desc = desc ; return this ; } tag ( tag ) { this . context . tags . push ( tag ) ; return this ; } enableWhen ( enableWhen ) { this . context . anyValueChanged = enableWhen ; return this ; } text ( initValue ) { return new TextSettingModelBuilder ( this . context , false , initValue ) ; } textArea ( initValue ) { return new TextSettingModelBuilder ( this . context , true , initValue ) ; } number ( initValue ) { return new NumberSettingModelBuilder ( this . context , initValue ) ; } toggle ( initValue ) { return new ToggleSettingModelBuilder ( this . context , initValue ) ; } dropdown ( initValue ) { return new DropdownSettingModelBuilder ( this . context , initValue ) ; } } interface Serde < R , E > { unmarshal ( rawValue ) : E ; marshal ( value ) : R ; } type AnyValueChanged = ( context : SettingContext ) => void ; abstract class AbstractSettingModelBuilder < R > { constructor ( protected context , protected initValue ) { } ; onAnyValueChanged ( anyValueChanged ) { this . context . anyValueChanged = anyValueChanged ; return this ; } abstract build < E > ( serde ) ; protected onValueChange ( ) { this . context . registry . forEach ( c => { c . update ( ) ; } ) ; } protected buildSettingModel < E > ( serde , initializer ) { return new SettingModelImpl ( this . context , serde , this . initValue , initializer ) ; } } class TextSettingModelBuilder extends AbstractSettingModelBuilder < string > { private _placeHolder ? : string ; constructor ( context , private longText , initValue ) { super ( context , initValue ) ; } placeHolder ( placeHolder ) { this . _placeHolder = placeHolder ; return this ; } build < E > ( serde ) { return this . buildSettingModel ( serde , ( { setting , rawValue , context } ) => { const initText = ( text ) => { text . setPlaceholder ( this . _placeHolder ?? "" ) . setValue ( rawValue . value ) . onChange ( async ( value ) => { try { serde . unmarshal ( value ) ; rawValue . value = value ; context . setValidationError ( null ) ; this . onValueChange ( ) ; } catch ( e ) { if ( e instanceof Error ) { context . setValidationError ( e . message ) ; } else if ( typeof e === "string" ) { context . setValidationError ( e ) ; } } } ) ; } ; if ( this . longText ) { setting . addTextArea ( ( textarea ) => { initText ( textarea ) ; } ) ; } else { setting . addText ( ( text ) => { initText ( text ) ; } ) ; } } ) ; } } class NumberSettingModelBuilder extends AbstractSettingModelBuilder < number > { private _placeHolder ? : string ; constructor ( context , initValue ) { super ( context , initValue ) ; } placeHolder ( placeHolder ) { this . _placeHolder = placeHolder ; return this ; } build < E > ( serde ) { return this . buildSettingModel ( serde , ( { setting , rawValue , context } ) => { const initText = ( text ) => { text . setPlaceholder ( this . _placeHolder ?? "" ) . setValue ( rawValue . value . toString ( ) ) . onChange ( async ( value ) => { try { const n = parseInt ( value ) ; rawValue . value = n ; context . setValidationError ( null ) ; this . onValueChange ( ) ; } catch ( e ) { if ( e instanceof Error ) { context . setValidationError ( e . message ) ; } else if ( typeof e === "string" ) { context . setValidationError ( e ) ; } } } ) ; } ; setting . addText ( ( textarea ) => { initText ( textarea ) ; } ) ; } ) ; } } class ToggleSettingModelBuilder extends AbstractSettingModelBuilder < boolean > { build < E > ( serde ) { return new SettingModelImpl ( this . context , serde , this . initValue , ( { setting , rawValue } ) => { setting . addToggle ( ( toggle ) => toggle . setValue ( rawValue . value ) . onChange ( async ( value ) => { rawValue . value = value ; this . onValueChange ( ) ; } ) ) ; } ) ; } } class DropdownOption { constructor ( public label , public value ) { } } class DropdownSettingModelBuilder < E > extends AbstractSettingModelBuilder < string > { private options : Array < DropdownOption > = [ ] ; addOption ( label , value ) { this . options . push ( new DropdownOption ( label , value ) ) ; return this ; } build < E > ( serde ) { return new SettingModelImpl ( this . context , serde , this . initValue , ( { setting , rawValue } ) => { setting . addDropdown ( d => { this . options . forEach ( option => { d . addOption ( option . value , option . label ) ; } ) ; d . setValue ( rawValue . value ) ; d . onChange ( async ( value ) => { rawValue . value = value ; this . onValueChange ( ) ; } ) ; } ) ; } ) ; } } export interface SettingModel < R , E > extends ReadOnlyReference < E > { rawValue : Reference < R > ; readonly key : string ; createSetting ( containerEl ) : Setting ; load ( settings ) : void ; store ( settings ) : void ; hasTag ( tag ) : boolean ; } type SettingInitilizer < R > = ( { setting , rawValue , context } : { setting : Setting ; rawValue : Reference < R > ; context : SettingContext ; } ) => void ; class SettingModelImpl < R , E > implements SettingModel < R , E > { rawValue : Reference < R > ; constructor ( private context , private serde , initRawValue , private settingInitializer ) { this . rawValue = new Reference ( initRawValue ) ; if ( context . key == null ) { throw new Error ( "key is required." ) ; } } createSetting ( containerEl ) { const setting = new Setting ( containerEl ) . setName ( this . context . name ?? "" ) . setDesc ( this . context . desc ?? "" ) ; this . context . init ( this , setting , containerEl ) ; this . settingInitializer ( { setting , rawValue : this . rawValue , context : this . context } ) ; return setting ; } get value ( ) : E { return this . serde . unmarshal ( this . rawValue . value ) ; } get key ( ) { return this . context . key ! ; } load ( settings ) { if ( settings === undefined ) { return ; } const newValue = settings [ this . key ] ; if ( newValue !== undefined ) { this . rawValue . value = newValue ; } } store ( settings ) { settings [ this . key ] = this . rawValue . value ; } hasTag ( tag ) { return this . context . hasTag ( tag ) ; } } export class SettingGroup { public settings : Array < SettingModel < any , any > > = [ ] ; constructor ( public name ) { } addSettings ( ... settingModels ) { this . settings . push ( ... settingModels ) ; } } export class SettingTabModel { private groups : Array < SettingGroup > = [ ] ; private registry : SettingRegistry = new SettingRegistry ( ) ; newSettingBuilder ( ) { return new SettingModelBuilder ( this . registry ) ; } newGroup ( name ) { const group = new SettingGroup ( name ) ; this . groups . push ( group ) ; return group ; } displayOn ( el ) { el . empty ( ) ; this . groups . forEach ( group => { el . createEl ( 'h3' , { text : group . name } ) ; group . settings . forEach ( settings => { settings . createSetting ( el ) ; } ) ; } ) ; this . registry . forEach ( context => context . update ( ) ) ; } public forEach ( consumer ) { this . groups . forEach ( group => { group . settings . forEach ( setting => { consumer ( setting ) ; } ) ; } ) ; } } export class TimeSerde implements Serde < string , Time > { unmarshal ( rawValue ) { return Time . parse ( rawValue ) ; } marshal ( value ) { return value . toString ( ) ; } } export class RawSerde < R > implements Serde < R , R > { unmarshal ( rawValue ) { return rawValue ; } marshal ( value ) { return value ; } } export class LatersSerde implements Serde < string , Array < Later > > { unmarshal ( rawValue ) { return parseLaters ( rawValue ) ; } marshal ( value ) { return value . map ( v => v . label ) . join ( "\n" ) ; } } export class ReminderFormatTypeSerde implements Serde < string , ReminderFormatType > { unmarshal ( rawValue ) { const format = ReminderFormatTypes . find ( format => format . name === rawValue ) ! ; return format ; } marshal ( value ) { return value . name ; } }