@@ -6,6 +6,8 @@ import type { Props } from "@zag-js/signature-pad";
66import { getBoolean , getNumber , getString } from "../lib/util" ;
77import { idMatches , readPayloadId } from "../lib/respond-to" ;
88
9+ const PHX_HAS_FOCUSED = "phx-has-focused" ;
10+
911function parsePathsFromDataset ( el : HTMLElement , key : "defaultPaths" | "paths" ) : string [ ] {
1012 const raw = el . dataset [ key ] ;
1113 if ( ! raw ) return [ ] ;
@@ -15,6 +17,12 @@ function parsePathsFromDataset(el: HTMLElement, key: "defaultPaths" | "paths"):
1517 . filter ( Boolean ) ;
1618}
1719
20+ function reapplyLiveViewValueInputUsage ( input : HTMLInputElement ) {
21+ const p = input as HTMLInputElement & { phxPrivate ?: Record < string , boolean > } ;
22+ if ( ! p . phxPrivate ) p . phxPrivate = { } ;
23+ p . phxPrivate [ PHX_HAS_FOCUSED ] = true ;
24+ }
25+
1826function buildDrawingOptions ( el : HTMLElement ) : NonNullable < Props [ "drawing" ] > {
1927 const o : Record < string , unknown > = {
2028 fill : getString ( el , "drawingFill" ) ,
@@ -29,16 +37,24 @@ function buildDrawingOptions(el: HTMLElement): NonNullable<Props["drawing"]> {
2937 return o as NonNullable < Props [ "drawing" ] > ;
3038}
3139
32- function queueFormBubblingInputForPhoenix ( el : HTMLElement , getValue : ( ) => string ) : void {
40+ function queueFormBubblingInputForPhoenix (
41+ el : HTMLElement ,
42+ getValue : ( ) => string ,
43+ opts : { onPadTouched : ( ) => void }
44+ ) : void {
3345 queueMicrotask ( ( ) => {
3446 const input = el . querySelector < HTMLInputElement > (
3547 '[data-scope="signature-pad"][data-part="hidden-input"]'
3648 ) ;
37- if ( ! input ) return ;
49+ if ( ! input ) {
50+ return ;
51+ }
3852 const v = getValue ( ) ;
3953 if ( String ( input . value ) !== String ( v ) ) {
4054 input . value = v ;
4155 }
56+ opts . onPadTouched ( ) ;
57+ reapplyLiveViewValueInputUsage ( input ) ;
4258 input . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
4359 input . dispatchEvent ( new Event ( "change" , { bubbles : true } ) ) ;
4460 } ) ;
@@ -48,14 +64,34 @@ type SignaturePadHookState = {
4864 signaturePad ?: SignaturePad ;
4965 handlers ?: Array < CallbackRef > ;
5066 onClear ?: ( event : Event ) => void ;
67+ padTouched : boolean ;
5168} ;
5269
5370const SignaturePadHook : Hook < object & SignaturePadHookState , HTMLElement > = {
5471 mounted ( this : object & HookInterface < HTMLElement > & SignaturePadHookState ) {
5572 const el = this . el ;
73+ const hook = this as object & SignaturePadHookState ;
5674 const pushEvent = this . pushEvent . bind ( this ) ;
75+ hook . padTouched = false ;
76+ const markTouched = ( ) => {
77+ hook . padTouched = true ;
78+ } ;
5779
5880 const defaultPaths = parsePathsFromDataset ( el , "defaultPaths" ) ;
81+ {
82+ const input = el . querySelector < HTMLInputElement > (
83+ '[data-scope="signature-pad"][data-part="hidden-input"]'
84+ ) ;
85+ if ( String ( input ?. value ?? "" ) !== "" || defaultPaths . length > 0 ) {
86+ hook . padTouched = true ;
87+ queueMicrotask ( ( ) => {
88+ const i = el . querySelector < HTMLInputElement > (
89+ '[data-scope="signature-pad"][data-part="hidden-input"]'
90+ ) ;
91+ if ( i ) reapplyLiveViewValueInputUsage ( i ) ;
92+ } ) ;
93+ }
94+ }
5995
6096 const signaturePad = new SignaturePad ( el , {
6197 id : el . id ,
@@ -65,8 +101,10 @@ const SignaturePadHook: Hook<object & SignaturePadHookState, HTMLElement> = {
65101 onDrawEnd : ( details ) => {
66102 signaturePad . setPaths ( details . paths ) ;
67103
68- queueFormBubblingInputForPhoenix ( el , ( ) =>
69- details . paths . length > 0 ? details . paths . join ( "\n" ) : ""
104+ queueFormBubblingInputForPhoenix (
105+ el ,
106+ ( ) => ( details . paths . length > 0 ? details . paths . join ( "\n" ) : "" ) ,
107+ { onPadTouched : markTouched }
70108 ) ;
71109
72110 details . getDataUrl ( "image/png" ) . then ( ( url ) => {
@@ -99,12 +137,11 @@ const SignaturePadHook: Hook<object & SignaturePadHookState, HTMLElement> = {
99137 } as Props ) ;
100138 signaturePad . init ( ) ;
101139 this . signaturePad = signaturePad ;
102-
103140 this . onClear = ( event : Event ) => {
104141 const { id : targetId } = ( event as CustomEvent < { id : string } > ) . detail ;
105142 if ( targetId && targetId !== el . id ) return ;
106143 signaturePad . api . clear ( ) ;
107- queueFormBubblingInputForPhoenix ( el , ( ) => "" ) ;
144+ queueFormBubblingInputForPhoenix ( el , ( ) => "" , { onPadTouched : markTouched } ) ;
108145 } ;
109146 el . addEventListener ( "corex:signature-pad:clear" , this . onClear ) ;
110147
@@ -114,14 +151,13 @@ const SignaturePadHook: Hook<object & SignaturePadHookState, HTMLElement> = {
114151 this . handleEvent ( "signature_pad_clear" , ( payload : unknown ) => {
115152 if ( ! idMatches ( el . id , readPayloadId ( payload ) ) ) return ;
116153 signaturePad . api . clear ( ) ;
117- queueFormBubblingInputForPhoenix ( el , ( ) => "" ) ;
154+ queueFormBubblingInputForPhoenix ( el , ( ) => "" , { onPadTouched : markTouched } ) ;
118155 } )
119156 ) ;
120157 } ,
121158
122159 updated ( this : object & HookInterface < HTMLElement > & SignaturePadHookState ) {
123160 const el = this . el ;
124- const defaultPaths = parsePathsFromDataset ( el , "defaultPaths" ) ;
125161 const name = getString ( el , "name" ) ;
126162
127163 if ( name ) {
@@ -131,9 +167,20 @@ const SignaturePadHook: Hook<object & SignaturePadHookState, HTMLElement> = {
131167 this . signaturePad ?. updateProps ( {
132168 id : el . id ,
133169 name : name ,
134- ...( defaultPaths . length > 0 ? { defaultPaths } : { } ) ,
135170 drawing : buildDrawingOptions ( el ) ,
136171 } as Partial < Props > ) ;
172+
173+ if ( ! this . padTouched ) {
174+ return ;
175+ }
176+ queueMicrotask ( ( ) => {
177+ const input = this . el . querySelector < HTMLInputElement > (
178+ '[data-scope="signature-pad"][data-part="hidden-input"]'
179+ ) ;
180+ if ( input ) {
181+ reapplyLiveViewValueInputUsage ( input ) ;
182+ }
183+ } ) ;
137184 } ,
138185
139186 destroyed ( this : object & HookInterface < HTMLElement > & SignaturePadHookState ) {
0 commit comments