nodebb-plugin-onekite-calendar 2.0.3 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog – calendar-onekite
2
2
 
3
+ ## 1.2.12
4
+ - Modales (calendrier + ACP) : autocomplete adresse rendu identique et compatible Bootstrap input-group (plus de wrapper qui casse l’affichage)
5
+
6
+ ## 1.2.11
7
+ - ACP : ajout de la recherche automatique d’adresse (autocomplete Nominatim) dans la modale de validation (unitaire + batch), comme sur le calendrier
8
+
3
9
  ## 1.2.10
4
10
  - HelloAsso : calcul des jours 100% fiable (différence en jours calendaires Y/M/J, sans dépendance aux heures/au fuseau/DST)
5
11
  - FullCalendar : endDate traitée comme exclusive partout (UI + checkout)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "2.0.3"
42
+ "version": "2.0.5"
43
43
  }
package/public/admin.js CHANGED
@@ -187,6 +187,154 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
187
187
  return { lat, lon, displayName: hit.display_name || q };
188
188
  }
189
189
 
190
+ // Lightweight address autocomplete (OpenStreetMap Nominatim)
191
+ // Designed to work inside Bootstrap "input-group" without moving DOM nodes.
192
+ async function searchAddresses(query, limit) {
193
+ const q = String(query || '').trim();
194
+ const lim = Math.min(10, Math.max(1, Number(limit) || 6));
195
+ if (q.length < 3) return [];
196
+ const url = `https://nominatim.openstreetmap.org/search?format=jsonv2&addressdetails=1&limit=${lim}&q=${encodeURIComponent(q)}`;
197
+ const res = await fetch(url, {
198
+ method: 'GET',
199
+ headers: {
200
+ 'Accept': 'application/json',
201
+ 'Accept-Language': 'fr',
202
+ },
203
+ });
204
+ if (!res.ok) return [];
205
+ const arr = await res.json();
206
+ if (!Array.isArray(arr)) return [];
207
+ return arr.map((hit) => {
208
+ const lat = Number(hit.lat);
209
+ const lon = Number(hit.lon);
210
+ return {
211
+ displayName: hit.display_name || q,
212
+ lat: Number.isFinite(lat) ? lat : null,
213
+ lon: Number.isFinite(lon) ? lon : null,
214
+ };
215
+ }).filter(h => h && h.displayName);
216
+ }
217
+
218
+ function attachAddressAutocomplete(inputEl, onPick) {
219
+ if (!inputEl) return;
220
+ if (inputEl.getAttribute('data-onekite-autocomplete') === '1') return;
221
+ inputEl.setAttribute('data-onekite-autocomplete', '1');
222
+
223
+ // In Bootstrap input-groups (especially in ACP), wrapping the input breaks layout.
224
+ // So we anchor the menu to the closest input-group (or parent) without moving the input.
225
+ const anchor = inputEl.closest && inputEl.closest('.input-group')
226
+ ? inputEl.closest('.input-group')
227
+ : (inputEl.parentNode || document.body);
228
+
229
+ try {
230
+ const cs = window.getComputedStyle(anchor);
231
+ if (!cs || cs.position === 'static') {
232
+ anchor.style.position = 'relative';
233
+ }
234
+ } catch (e) {
235
+ anchor.style.position = 'relative';
236
+ }
237
+
238
+ const menu = document.createElement('div');
239
+ menu.className = 'onekite-autocomplete-menu';
240
+ menu.style.position = 'absolute';
241
+ menu.style.left = '0';
242
+ menu.style.right = '0';
243
+ menu.style.top = '100%';
244
+ menu.style.zIndex = '2000';
245
+ menu.style.background = '#fff';
246
+ menu.style.border = '1px solid rgba(0,0,0,.15)';
247
+ menu.style.borderTop = '0';
248
+ menu.style.maxHeight = '220px';
249
+ menu.style.overflowY = 'auto';
250
+ menu.style.display = 'none';
251
+ menu.style.borderRadius = '0 0 .375rem .375rem';
252
+ anchor.appendChild(menu);
253
+
254
+ let timer = null;
255
+ let lastQuery = '';
256
+ let busy = false;
257
+
258
+ function hide() {
259
+ menu.style.display = 'none';
260
+ menu.innerHTML = '';
261
+ }
262
+
263
+ function show(hits) {
264
+ if (!hits || !hits.length) {
265
+ hide();
266
+ return;
267
+ }
268
+ menu.innerHTML = '';
269
+ hits.forEach((h) => {
270
+ const btn = document.createElement('button');
271
+ btn.type = 'button';
272
+ btn.className = 'onekite-autocomplete-item';
273
+ btn.textContent = h.displayName;
274
+ btn.style.display = 'block';
275
+ btn.style.width = '100%';
276
+ btn.style.textAlign = 'left';
277
+ btn.style.padding = '.35rem .5rem';
278
+ btn.style.border = '0';
279
+ btn.style.background = 'transparent';
280
+ btn.style.cursor = 'pointer';
281
+ btn.addEventListener('click', () => {
282
+ inputEl.value = h.displayName;
283
+ hide();
284
+ try { onPick && onPick(h); } catch (e) {}
285
+ });
286
+ btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(0,0,0,.05)'; });
287
+ btn.addEventListener('mouseleave', () => { btn.style.background = 'transparent'; });
288
+ menu.appendChild(btn);
289
+ });
290
+ menu.style.display = 'block';
291
+ }
292
+
293
+ async function run(q) {
294
+ if (busy) return;
295
+ busy = true;
296
+ try {
297
+ const hits = await searchAddresses(q, 6);
298
+ if (String(inputEl.value || '').trim() !== q) return; // ignore stale
299
+ show(hits);
300
+ } catch (e) {
301
+ hide();
302
+ } finally {
303
+ busy = false;
304
+ }
305
+ }
306
+
307
+ inputEl.addEventListener('input', () => {
308
+ const q = String(inputEl.value || '').trim();
309
+ lastQuery = q;
310
+ if (timer) clearTimeout(timer);
311
+ if (q.length < 3) {
312
+ hide();
313
+ return;
314
+ }
315
+ timer = setTimeout(() => run(lastQuery), 250);
316
+ });
317
+
318
+ inputEl.addEventListener('focus', () => {
319
+ const q = String(inputEl.value || '').trim();
320
+ if (q.length >= 3) {
321
+ if (timer) clearTimeout(timer);
322
+ timer = setTimeout(() => run(q), 150);
323
+ }
324
+ });
325
+
326
+ inputEl.addEventListener('keydown', (e) => {
327
+ if (e.key === 'Escape') hide();
328
+ });
329
+
330
+ // Close when clicking outside (once per menu)
331
+ document.addEventListener('click', (e) => {
332
+ try {
333
+ if (!anchor.contains(e.target)) hide();
334
+ } catch (err) {}
335
+ });
336
+ }
337
+
190
338
  function formToObject(form) {
191
339
  const out = {};
192
340
  new FormData(form).forEach((v, k) => {
@@ -698,6 +846,19 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
698
846
  } catch (e) {}
699
847
  });
700
848
  }
849
+
850
+ // Autocomplete like on the calendar validation modal
851
+ const addrInput = document.getElementById('onekite-pickup-address');
852
+ attachAddressAutocomplete(addrInput, (h) => {
853
+ try {
854
+ if (!h || !Number.isFinite(Number(h.lat)) || !Number.isFinite(Number(h.lon))) return;
855
+ const lat = Number(h.lat);
856
+ const lon = Number(h.lon);
857
+ marker.setLatLng([lat, lon]);
858
+ map.setView([lat, lon], 16);
859
+ setLatLon(lat, lon);
860
+ } catch (e) {}
861
+ });
701
862
  setTimeout(() => { try { map.invalidateSize(); } catch (e) {} }, 250);
702
863
  } catch (e) {}
703
864
  });
@@ -887,6 +1048,14 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
887
1048
  }
888
1049
  });
889
1050
  }
1051
+
1052
+ // Autocomplete like on the calendar validation modal
1053
+ attachAddressAutocomplete(addrInput, (h) => {
1054
+ try {
1055
+ if (!h || !Number.isFinite(Number(h.lat)) || !Number.isFinite(Number(h.lon))) return;
1056
+ setMarker(Number(h.lat), Number(h.lon), 16);
1057
+ } catch (e) {}
1058
+ });
890
1059
  } catch (e) {
891
1060
  // ignore
892
1061
  }
package/public/client.js CHANGED
@@ -638,10 +638,20 @@ function attachAddressAutocomplete(inputEl, onPick) {
638
638
  if (inputEl.getAttribute('data-onekite-autocomplete') === '1') return;
639
639
  inputEl.setAttribute('data-onekite-autocomplete', '1');
640
640
 
641
- const wrapper = document.createElement('div');
642
- wrapper.style.position = 'relative';
643
- inputEl.parentNode && inputEl.parentNode.insertBefore(wrapper, inputEl);
644
- wrapper.appendChild(inputEl);
641
+ // In Bootstrap input-groups (especially in ACP), wrapping the input breaks layout.
642
+ // So we anchor the menu to the closest input-group (or parent) without moving the input.
643
+ const host = inputEl.closest && inputEl.closest('.input-group')
644
+ ? inputEl.closest('.input-group')
645
+ : (inputEl.parentNode || document.body);
646
+
647
+ try {
648
+ const cs = window.getComputedStyle(host);
649
+ if (!cs || cs.position === 'static') {
650
+ host.style.position = 'relative';
651
+ }
652
+ } catch (e) {
653
+ host.style.position = 'relative';
654
+ }
645
655
 
646
656
  const menu = document.createElement('div');
647
657
  menu.className = 'onekite-autocomplete-menu';
@@ -657,7 +667,7 @@ function attachAddressAutocomplete(inputEl, onPick) {
657
667
  menu.style.overflowY = 'auto';
658
668
  menu.style.display = 'none';
659
669
  menu.style.borderRadius = '0 0 .375rem .375rem';
660
- wrapper.appendChild(menu);
670
+ host.appendChild(menu);
661
671
 
662
672
  let timer = null;
663
673
  let lastQuery = '';
@@ -735,7 +745,7 @@ function attachAddressAutocomplete(inputEl, onPick) {
735
745
  // Close when clicking outside
736
746
  document.addEventListener('click', (e) => {
737
747
  try {
738
- if (!wrapper.contains(e.target)) hide();
748
+ if (!host.contains(e.target)) hide();
739
749
  } catch (err) {}
740
750
  });
741
751