diff --git a/check/list/list.view.tree b/check/list/list.view.tree index a79c5591e57..4ea590a27d8 100644 --- a/check/list/list.view.tree +++ b/check/list/list.view.tree @@ -1,4 +1,5 @@ $mol_check_list $mol_view + dictionary * Option* $mol_check checked? <=> option_checked*? false label <= option_label* / diff --git a/check/list/list.view.ts b/check/list/list.view.ts index d338b08fce1..a739b1beb72 100644 --- a/check/list/list.view.ts +++ b/check/list/list.view.ts @@ -10,6 +10,20 @@ namespace $.$$ { return {} } + override dictionary(next?: Record) { + return next ?? {} + } + + override option_checked(id: string, next?: boolean | null) { + const prev = this.dictionary() + if (next === undefined) return prev[id] ?? null + + const next_rec = { ... prev, [id]: next } as Record + if (next === null) delete next_rec[id] + + return this.dictionary(next_rec)[id] ?? null + } + @ $mol_mem keys(): readonly string[] { return Object.keys( this.options() ) diff --git a/form/draft/demo/demo.view.tree b/form/draft/demo/demo.view.tree index e5c964ae24f..9f6e8501f95 100644 --- a/form/draft/demo/demo.view.tree +++ b/form/draft/demo/demo.view.tree @@ -3,6 +3,8 @@ $mol_form_draft_demo_article $mol_object2 type? \ adult? false content? \ + friends? /string + hobbies? * $mol_form_draft_demo $mol_example title \Article draft form demo @@ -19,7 +21,10 @@ $mol_form_draft_demo $mol_example submit? => publish? submit_allowed => publish_allowed value_str*? => value_str*? + list_string*? => list_string*? + dictionary_bool*? => dictionary_bool*? changed => changed + reset? => reset? form_fields / <= Title_field $mol_form_field name \Title @@ -54,12 +59,34 @@ $mol_form_draft_demo $mol_example Content <= Content $mol_textarea hint \Long long story.. value? <=> value_str*content? + <= Hobbies_field $mol_form_field + name \Hobbies + Content <= Hobbies $mol_check_list + dictionary? <=> dictionary_bool*hobbies? + options * + programming \Programming + bikinkg \Biking + fishing \Fishing + <= Friends_field $mol_form_field + name \Friends + Content <= Friends $mol_select_list + dictionary * + jocker \Jocker + harley \Harley Quinn + penguin \Penguin + riddler \Riddler + bane \Bane + freeze \Mister Freeze + clay \Clayface + mask \Black Mask + value? <=> list_string*friends? body <= form_body / <= Title_field <= Config $mol_form_group sub / <= Adult_field <= Type_field <= Content_field + <= Friends_field buttons / <= Publish $mol_button_major title \Publish @@ -67,6 +94,10 @@ $mol_form_draft_demo $mol_example enabled <= publish_allowed <= Result $mol_status message <= result? \ + <= Reset $mol_button_minor + title \Сбросить + click? <=> reset? + enabled <= changed tags / \$mol_form_field \$mol_button diff --git a/form/draft/demo/demo.view.ts b/form/draft/demo/demo.view.ts index 16c4c604b76..68f8884791f 100644 --- a/form/draft/demo/demo.view.ts +++ b/form/draft/demo/demo.view.ts @@ -7,7 +7,9 @@ namespace $.$$ { return [ this.Title_field(), this.Config(), + this.Hobbies_field(), ... this.value_str( 'type' ) ? [ this.Content_field() ] : [], + this.Friends_field(), ] } @@ -42,6 +44,6 @@ namespace $.$$ { super.publish() this.result( this.message_done() ) } - + } } diff --git a/form/draft/draft.view.ts b/form/draft/draft.view.ts index 025fea04a42..07e3be0634d 100644 --- a/form/draft/draft.view.ts +++ b/form/draft/draft.view.ts @@ -1,61 +1,131 @@ namespace $.$$ { + type Primitive = string | number | boolean + + type Value = readonly Primitive[] | Primitive | Record + type Model = Record Value> + + function norm_string(val: unknown) { + return String(val ?? '') + } + + function norm_number(val: unknown) { + return Number(val ?? 0) + } + + function norm_bool(val: unknown) { + return Boolean(val ?? false) + } + + function normalize_val(prev: Value, next: Value | null) { + switch( typeof prev ) { + case 'boolean': return String( next ) === 'true' + case 'number': return Number( next ) + case 'string': return String( next ) + } + + return next + } + /** * @see https://mol.hyoo.ru/#!section=demos/demo=mol_form_draft_demo */ export class $mol_form_draft extends $.$mol_form_draft { - + @ $mol_mem_key + list_string( field: string, next? : readonly string[] | null ) { + return this.value( field, next )?.map(norm_string) ?? [] + } + + @ $mol_mem_key + dictionary_bool( field: string, next? : Record | null ): Record { + if (next) { + const prev = this.model_pick(field) as Record + const normalized = {} as typeof next + for (const key in next) { + if (next[key] || key in prev ) normalized[key] = next[key] + } + + return this.value( field, normalized ) ?? {} + } + + return this.value( field ) ?? {} + } + @ $mol_mem_key value_str( field: string, next? : string | null ) { - return String( this.value( field, next ) ?? '' ) + return norm_string( this.value( field, next ) ) } @ $mol_mem_key value_numb( field: string, next? : boolean | null ) { - return Number( this.value( field, next ) ?? 0 ) + return norm_number( this.value( field, next ) ) } @ $mol_mem_key value_bool( field: string, next? : boolean | null ) { - return Boolean( this.value( field, next ) ?? false ) + return norm_bool( this.value( field, next ) ) } - + + model_pick(field: string, next?: Value | null) { + return (this.model() as unknown as Model)[field](next) + } + + state_pick(field: string, next?: Value | null) { + return this.state( next === undefined ? next : { ... this.state(), [ field ]: next } )[ field ] + } + @ $mol_mem_key - value( field: string, next?: string | number | boolean | null ) { - return this.state( next?.valueOf && { ... this.state(), [ field ]: next } )[ field ] - ?? ( this.model() as any )[ field ]() + value( field: string, next?: T | null ): T { + if (Array.isArray(next) && next.length === 0 && ! this.model_pick(field)) next = null + return this.state_pick(field, next) as T ?? this.model_pick(field) + } + + @ $mol_mem_key + value_changed(field: string) { + const next = this.state_pick(field) + const prev = this.model_pick(field) + const next_norm = normalize_val(prev, next) + + return ! $mol_compare_deep(next_norm, prev) } @ $mol_mem - state( next?: Record< string, string | number | boolean | null > | null ) { + state( next?: Record< string, Value | null > | null ) { return $mol_state_local.value( `${ this }.state()`, next ) ?? {} } @ $mol_mem changed() { - return Object.keys( this.state() ).length > 0 + return Object.keys(this.state()).some(field => this.value_changed(field)) } submit_allowed() { return this.changed() && super.submit_allowed() } - + + reset(next?: unknown) { + this.state(null) + } + @ $mol_action submit( next? : Event ) { - const model = this.model() - - for( let [ field, next ] of Object.entries( this.state() ) ) { - const prev = ( model as any )[ field ]() - switch( typeof prev ) { - case 'boolean': next = String( next ) === 'true'; break - case 'number': next = Number( next ); break - case 'string': next = String( next ); break + const tasks = Object.entries( this.state() ).map( + ([ field, next ]) => () => { + const prev = this.model_pick(field) + + return { + field, + next: normalize_val(prev, next) + } } - ;( model as any )[ field ]( next ) - } + ) + + const normalized = $mol_wire_race(...tasks) + + $mol_wire_race(...normalized.map(({ field, next }) => () => this.model_pick( field, next ))) - this.state( null ) + this.reset() } diff --git a/list/list.view.css b/list/list.view.css index 0c0ddfcd83d..53c0104edcf 100644 --- a/list/list.view.css +++ b/list/list.view.css @@ -8,7 +8,7 @@ align-items: stretch; align-content: stretch; */ transition: none; - min-height: .5rem; + min-height: 1.5rem; } [mol_list_gap_before] , diff --git a/select/list/demo/demo.view.tree b/select/list/demo/demo.view.tree index f4e3ebce76d..27b3772f75f 100644 --- a/select/list/demo/demo.view.tree +++ b/select/list/demo/demo.view.tree @@ -18,6 +18,12 @@ $mol_select_list_demo $mol_example_small value? <=> friends? / dictionary <= suggestions enabled false + <= Friends_lazy $mol_select_list + value? <=> friends_lazy? / + option_title* <= option_title* \ + filter_pattern? => filter_pattern? + pick_enabled true + dictionary <= suggestions_lazy <= suggestions tags / \select \tags diff --git a/select/list/demo/demo.view.ts b/select/list/demo/demo.view.ts new file mode 100644 index 00000000000..3baf04b0035 --- /dev/null +++ b/select/list/demo/demo.view.ts @@ -0,0 +1,15 @@ +namespace $.$$ { + export class $mol_select_list_demo extends $.$mol_select_list_demo { + @ $mol_mem + override suggestions_lazy() { + this.$.$mol_wait_timeout(500) + this.filter_pattern() + return super.suggestions() + } + + override option_title(id: string) { + if (! id) return '' + return this.suggestions_lazy()[id] + } + } +} diff --git a/select/list/list.view.tree b/select/list/list.view.tree index cf74546842b..2965b04af34 100644 --- a/select/list/list.view.tree +++ b/select/list/list.view.tree @@ -9,11 +9,13 @@ $mol_select_list $mol_view enabled <= drop_enabled <= enabled true sub /$mol_view <= Pick $mol_select + event_select*? <=> event_select*? null align_hor <= align_hor \right options <= options_pickable <= options /string value? <=> pick? \ option_label* <= option_title* \ trigger_enabled <= pick_enabled <= enabled true hint <= pick_hint @ \Add.. + filter_pattern? => filter_pattern? Trigger_icon <= Pick_icon $mol_icon_plus ^ badges_list diff --git a/select/list/list.view.ts b/select/list/list.view.ts index a7bdb457ef4..40222b4764d 100644 --- a/select/list/list.view.ts +++ b/select/list/list.view.ts @@ -6,7 +6,7 @@ namespace $.$$ { */ export class $mol_select_list extends $.$mol_select_list { - override value( val? : string[] ) { + override value( val? : readonly string[] ) { return super.value( val ) as readonly string[] } @@ -16,16 +16,14 @@ namespace $.$$ { if( !key ) return '' this.value([ ... this.value() , key ]) - new $mol_after_frame(()=> { - if( !this.pick_enabled() ) return - this.Pick().filter_pattern( '' ) - this.Pick().Trigger().focused( true ) - this.Pick().open() - }) - return '' } + override event_select( id : string , event? : MouseEvent ) { + event?.preventDefault() + this.pick( id ) + } + @ $mol_mem override options() { return Object.keys( this.dictionary() ) as readonly string[] @@ -46,8 +44,8 @@ namespace $.$$ { return value == null ? key : value } - override badge_title( index: number ) { - return this.option_title( this.value()[ index ] ) + override badge_title( key: string ) { + return this.option_title( key ) } @ $mol_mem @@ -57,7 +55,7 @@ namespace $.$$ { override Badges() { return this.value() - .map( ( _, index )=> this.Badge( index ) ) + .map( id => this.Badge( id ) ) .reverse() } @@ -67,12 +65,8 @@ namespace $.$$ { } @ $mol_action - override remove( index: number ) { - const value = this.value() - this.value([ - ... value.slice( 0 , index ), - ... value.slice( index + 1 ), - ]) + override remove( key: string ) { + this.value(this.value().filter(id => id !== key)) } } diff --git a/select/select.view.tree b/select/select.view.tree index d2d94a76c00..67c675ccac3 100644 --- a/select/select.view.tree +++ b/select/select.view.tree @@ -21,12 +21,13 @@ $mol_select $mol_pick hint @ \Pick.. bubble_content / <= Filter - <= Bubble_pane $mol_scroll sub / - <= Menu $mol_list - rows <= menu_content /$mol_view - Filter $mol_string - value? <=> filter_pattern? \ - hint @ \Filter.. + <= Bubble_pane $mol_scroll + sub / + <= Menu $mol_list + rows <= menu_content /$mol_view + Filter $mol_search + query? <=> filter_pattern? \ + hint <= filter_hint @ \Filter.. submit?event <=> submit?event null enabled <= enabled true Trigger_icon $mol_icon_dots_vertical