11{ if $attribute -> AppliesToEntity ($id )}
2+ { assign var= type value= $attribute -> Type ()}
23 { assign var= attributeId value= " inline{$attribute->Id()}{$id}" }
3- <div class =" updateCustomAttribute mb-0 col-12 col-sm-4" >
4- { assign var= datatype value= ' text' }
5- { if $attribute -> Type () == CustomAttributeTypes::CHECKBOX}
6- { assign var= datatype value= ' checklist' }
7- { elseif $attribute -> Type () == CustomAttributeTypes::MULTI_LINE_TEXTBOX}
8- { assign var= datatype value= ' textarea' }
9- { elseif $attribute -> Type () == CustomAttributeTypes::SELECT_LIST}
10- { assign var= datatype value= ' select' }
11- { elseif $attribute -> Type () == CustomAttributeTypes::DATETIME}
12- { assign var= datatype value= ' combodate' }
13- { assign var= value value= {formatdate date= $value key= fullcalendar} }
4+ { assign var= datatype value= ' text' }
5+ { assign var= inlineClass value= ' inlineAttribute' }
6+ { assign var= pickerValue value= $value }
7+
8+ { if $type == CustomAttributeTypes::CHECKBOX}
9+ { assign var= datatype value= ' checklist' }
10+ { elseif $type == CustomAttributeTypes::MULTI_LINE_TEXTBOX}
11+ { assign var= datatype value= ' textarea' }
12+ { elseif $type == CustomAttributeTypes::SELECT_LIST}
13+ { assign var= datatype value= ' select' }
14+ { elseif $type == CustomAttributeTypes::DATETIME}
15+ { assign var= AltFormat value= ' short_datetime' }
16+ { assign var= pickerControlId value= " inlinePickerInput{$attributeId}" }
17+ { assign var= inlineClass value= ' inlineAttributeDateTime' }
18+ { if $value != ' ' }
19+ { assign var= pickerValue value= {formatdate date= $value format= ' Y-m-d H:i' } }
1420 { /if }
21+ { /if }
22+
23+ <div class =" updateCustomAttribute mb-0 d-inline-block" >
1524 <label class =" inline fw-bold" >{ $attribute -> Label ()} </label >
16- <a class =" update changeAttribute link-primary" title =" { translate key= ' Edit' } " href =" #" ><span class =" bi bi-pencil-square" ></span >
17- <span class =" visually-hidden" >{ translate key= Edit} </span ></a >
18- <span class =" inlineAttribute" id =" inline{ $attributeId } " data-type =" { $datatype } " data-pk =" { $id } " data-value =" { $value } " data-name =" { FormKeys::ATTRIBUTE_PREFIX} { $attribute -> Id ()} " { if $attribute -> Type () == CustomAttributeTypes::SELECT_LIST} data-source =' [{ if ! $attribute -> Required ()} { ldelim } value:"",text:""{ rdelim } ,{ /if }
19- { foreach from= $attribute -> PossibleValueList () item= v name= vals}
20- { ldelim } value:"{ $v } ",text:"{ $v } "{ rdelim } { if not $smarty .foreach .vals.last} ,{ /if }
21- { /foreach } ]' { /if } { if $attribute -> Type () == CustomAttributeTypes::CHECKBOX} data-source =' [{ ldelim } value:"1",text:"{ translate key= Yes} "{ rdelim } ]'
22- { /if } ></span >
23- { if $attribute -> Type () == CustomAttributeTypes::DATETIME}
24- <script type =" text/javascript" >
25- $ (function () {
26- $(' #inline{ $attributeId } ' ).editable({
27- url: " { $url } " ,
28- viewformat: " { Resources::GetInstance()-> GetDateFormat (' momentjs_datetime' )} " ,
29- format: " YYYY-M-D H:m" ,
30- template: " { Resources::GetInstance()-> GetDateFormat (' momentjs_datetime' )} " ,
31- combodate: {
32- minYear: " { Date::Now()-> AddYears (-20)-> Format (' Y' )} " ,
33- maxYear: " { Date::Now()-> AddYears (20)-> Format (' Y' )} " ,
34- firstItem: " none"
35- } ,
36- emptytext: ' -' ,
37- emptyclass: ' ' ,
38- toggle: ' manual' ,
39- params: function(params) {
40- params.CSRF_TOKEN = $(' #csrf_token' ).val();
41- return params;
25+
26+ <a class =" update { if $type == CustomAttributeTypes::DATETIME} changeAttributeDateTime{ else } changeAttribute{ /if } link-primary"
27+ title =" { translate key= ' Edit' } " href =" #" >
28+ <span class =" bi bi-pencil-square" ></span >
29+ <span class =" visually-hidden" >{ translate key= Edit} </span >
30+ </a >
31+
32+ <span class =" { $inlineClass } update" id =" { $attributeId } " data-type =" { $datatype } " data-pk =" { $id } "
33+ data-value =" { $pickerValue |escape :' html' } " data-name =" { FormKeys::ATTRIBUTE_PREFIX} { $attribute -> Id ()} "
34+ { if $type == CustomAttributeTypes::SELECT_LIST} data-source =' [
35+ { if ! $attribute -> Required ()}
36+ { ldelim } "value":"","text":""{ rdelim } ,
37+ { /if }
38+ { foreach from= $attribute -> PossibleValueList () item= v name= vals}
39+ { ldelim } "value":{ $v |@json_encode|escape :' html' } ,"text":{ $v |@json_encode|escape :' html' } { rdelim } { if not $smarty .foreach .vals.last} ,{ /if }
40+ { /foreach }
41+ ]' { /if } { if $type == CustomAttributeTypes::CHECKBOX}
42+ data-source =' [{ ldelim } value:"1",text:"{ translate key= Yes} "{ rdelim } ]' { /if } >
43+ { if $type == CustomAttributeTypes::DATETIME}
44+ { if $value != ' ' } { formatdate date= $value key= $AltFormat } { else } -{ /if }
45+ { /if }
46+ </span >
47+
48+ { if $type == CustomAttributeTypes::DATETIME}
49+ <div class =" d-none position-absolute border rounded bg-body shadow p-2 z-3 update" id =" inlinePicker{ $attributeId } " >
50+
51+ <input type =" text" id =" { $pickerControlId } " class =" form-control form-control-sm" autocomplete =" off" >
52+ </div >
53+
54+ { control type= " DatePickerSetupControl" ControlId= $pickerControlId HasTimepicker= true Inline= true DefaultDate= $pickerValue AltFormat= $AltFormat }
55+
56+ <script >
57+ (function () {
58+ const display = document.getElementById(' { $attributeId } ' );
59+ const container = document.getElementById(' inlinePicker{ $attributeId } ' );
60+ const input = document.getElementById(' { $pickerControlId } ' );
61+ const button = display.closest(' .updateCustomAttribute' ).querySelector(' .changeAttributeDateTime' );
62+ const namespace = ' picker{ $attributeId } ' ;
63+ let pendingValue = display.dataset.value || ' ' ;
64+ let isSaving = false ;
65+
66+ // DatePickerSetupControl initializes flatpickr synchronously (whenFlatpickrReady
67+ // runs inline callbacks immediately when window.flatpickr is already loaded).
68+ // whenPickerReady is only a fallback for deferred script loading.
69+ function whenPickerReady(cb, retries = 60) {
70+ if (input._flatpickr) {
71+ cb(input._flatpickr);
72+ return;
73+ }
74+
75+ let attempts = 0;
76+ const id = setInterval(() => {
77+ if (input._flatpickr) {
78+ clearInterval(id);
79+ cb(input._flatpickr);
80+ return;
81+ }
82+
83+ attempts += 1;
84+ if (attempts >= retries) {
85+ clearInterval(id);
86+ console.warn(' InlineAttributeEdit: flatpickr not initialized for { $pickerControlId } ' );
87+ }
88+ } , 50);
89+ }
90+
91+ function show() { container.classList.remove(' d-none' ); }
92+
93+ function hide() { container.classList.add(' d-none' ); }
94+
95+ function extractErrorMessage(errors) {
96+ if (! errors) {
97+ return ' Error saving value' ;
98+ }
99+
100+ if (typeof errors === ' string' ) {
101+ return errors;
102+ }
103+
104+ if (Array.isArray(errors)) {
105+ return errors.join(' \n ' );
106+ }
107+
108+ if (typeof errors === ' object' ) {
109+ const messages = [];
110+ for (const key in errors) {
111+ if (! Object.prototype.hasOwnProperty.call(errors, key)) {
112+ continue;
113+ }
114+
115+ const value = errors[key];
116+ if (Array.isArray(value)) {
117+ messages.push(value.join(' , ' ));
118+ } else {
119+ messages.push(String(value));
120+ }
42121 }
122+
123+ return messages.length ? messages.join(' \n ' ) : ' Error saving value' ;
124+ }
125+
126+ return ' Error saving value' ;
127+ }
128+
129+ function save(value) {
130+ const body = new URLSearchParams({
131+ pk: ' { $id } ' ,
132+ name: ' { FormKeys::ATTRIBUTE_PREFIX} { $attribute -> Id ()} ' ,
133+ value: value,
134+ CSRF_TOKEN: (document.getElementById(' csrf_token' ) || { } ).value || ' '
135+ } );
136+
137+ fetch (" { $url } " , { method: ' POST' , body } )
138+ .then(function(response) {
139+ if (! response.ok) {
140+ throw new Error(' HTTP ' + response.status);
141+ }
142+
143+ return response.text();
144+ } )
145+ .then(function(rawBody) {
146+ const trimmedBody = rawBody ? rawBody.trim() : ' ' ;
147+ if (trimmedBody !== ' ' ) {
148+ let payload;
149+ try {
150+ payload = JSON.parse(trimmedBody);
151+ } catch (e) {
152+ payload = null;
153+ }
154+
155+ if (payload && typeof payload === ' object' && payload.errors) {
156+ throw new Error(extractErrorMessage(payload.errors));
157+ }
158+ }
159+
160+ const text = value === ' ' ? ' -' : (input._flatpickr?.altInput?.value || value);
161+ display.textContent = text;
162+ display.dataset.value = value;
163+ pendingValue = value;
164+ isSaving = false ;
165+ hide();
166+ } )
167+ .catch(function(error) {
168+ isSaving = false ;
169+ alert((error && error.message) ? error.message : ' Error saving value' );
170+ } );
171+ }
172+
173+ function saveIfChanged() {
174+ if (isSaving) {
175+ return;
176+ }
177+
178+ const currentValue = display.dataset.value || ' ' ;
179+ if (pendingValue === currentValue) {
180+ hide();
181+ return;
182+ }
183+
184+ isSaving = true ;
185+ save(pendingValue);
186+ }
187+
188+ whenPickerReady(function(picker) {
189+ picker.config.onChange.push(function(_, dateStr) {
190+ pendingValue = dateStr;
43191 } );
44192 } );
45- </script >
46- { /if }
47- </div >
48- { /if }
193+
194+ button.addEventListener(' click' , function(e) {
195+ e.preventDefault();
196+ e.stopPropagation();
197+ pendingValue = display.dataset.value || ' ' ;
198+ show();
199+ if (input._flatpickr) {
200+ input._flatpickr.setDate(display.dataset.value || null, false );
201+ }
202+ } );
203+
204+ // Close on outside click / ESC — keyed registry avoids duplicate listeners
205+ // when multiple pickers are rendered on the same page
206+ if (! document._pickerHandlers) document._pickerHandlers = { } ;
207+
208+ if (document._pickerHandlers[namespace + ' _mousedown' ]) {
209+ document.removeEventListener(' mousedown' , document._pickerHandlers[namespace + ' _mousedown' ]);
210+ }
211+ document._pickerHandlers[namespace + ' _mousedown' ] = function(e) {
212+ if (container.classList.contains(' d-none' )) return;
213+ if (container.contains(e.target) || button.contains(e.target)) return;
214+ saveIfChanged();
215+ } ;
216+ document.addEventListener(' mousedown' , document._pickerHandlers[namespace + ' _mousedown' ]);
217+
218+ if (document._pickerHandlers[namespace + ' _keydown' ]) {
219+ document.removeEventListener(' keydown' , document._pickerHandlers[namespace + ' _keydown' ]);
220+ }
221+ document._pickerHandlers[namespace + ' _keydown' ] = function(e) {
222+ if (e.key !== ' Escape' ) return;
223+ if (container.classList.contains(' d-none' )) return;
224+ pendingValue = display.dataset.value || ' ' ;
225+ if (input._flatpickr) {
226+ input._flatpickr.setDate(display.dataset.value || null, false );
227+ }
228+ hide();
229+ } ;
230+ document.addEventListener(' keydown' , document._pickerHandlers[namespace + ' _keydown' ]);
231+ } )();
232+ </script >
233+ { /if }
234+ </div >
235+ { /if }
0 commit comments