nodebb-plugin-onekite-calendar 1.0.10 → 1.0.12

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/lib/widgets.js DELETED
@@ -1,357 +0,0 @@
1
- 'use strict';
2
-
3
- const nconf = require.main.require('nconf');
4
-
5
- function forumBaseUrl() {
6
- return String(nconf.get('url') || '').trim().replace(/\/$/, '');
7
- }
8
-
9
- function escapeHtml(s) {
10
- return String(s || '')
11
- .replace(/&/g, '&')
12
- .replace(/</g, '&lt;')
13
- .replace(/>/g, '&gt;')
14
- .replace(/"/g, '&quot;')
15
- .replace(/'/g, '&#39;');
16
- }
17
-
18
- function makeDomId() {
19
- const r = Math.floor(Math.random() * 1e9);
20
- return `onekite-twoweeks-${Date.now()}-${r}`;
21
- }
22
-
23
- function widgetCalendarUrl() {
24
- // Per request, keep the public URL fixed (even if forum base differs)
25
- return 'https://www.onekite.com/calendar';
26
- }
27
-
28
- const widgets = {};
29
-
30
- widgets.defineWidgets = async function (widgetData) {
31
- // NodeBB versions differ:
32
- // - Some pass an object: { widgets: [...] }
33
- // - Others pass the array directly
34
- const list = Array.isArray(widgetData)
35
- ? widgetData
36
- : (widgetData && Array.isArray(widgetData.widgets) ? widgetData.widgets : null);
37
-
38
- if (!list) {
39
- return widgetData;
40
- }
41
-
42
- list.push({
43
- widget: 'calendar-onekite-twoweeks',
44
- name: 'Calendrier',
45
- description: 'Affiche la semaine courante + la semaine suivante (FullCalendar via CDN).',
46
- content: '',
47
- });
48
-
49
- return widgetData;
50
- };
51
-
52
- widgets.renderTwoWeeksWidget = async function (data) {
53
- // data: { widget, ... }
54
- const id = makeDomId();
55
- const calUrl = widgetCalendarUrl();
56
- const apiBase = forumBaseUrl();
57
- const eventsEndpoint = `${apiBase}/api/v3/plugins/calendar-onekite/events`;
58
-
59
- const idJson = JSON.stringify(id);
60
- const calUrlJson = JSON.stringify(calUrl);
61
- const eventsEndpointJson = JSON.stringify(eventsEndpoint);
62
-
63
- const html = `
64
- <div class="onekite-twoweeks">
65
- <div class="d-flex justify-content-between align-items-center mb-1">
66
- <div style="font-weight: 600;">Calendrier</div>
67
- <a href="${escapeHtml(calUrl)}" class="btn btn-sm btn-outline-secondary" style="line-height: 1.1;">Ouvrir</a>
68
- </div>
69
- <div id="${escapeHtml(id)}"></div>
70
- </div>
71
-
72
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@latest/main.min.css" />
73
- <script>
74
- (function(){
75
- const containerId = ${idJson};
76
- const calUrl = ${calUrlJson};
77
- const eventsEndpoint = ${eventsEndpointJson};
78
-
79
- function loadOnce(tag, attrs) {
80
- return new Promise((resolve, reject) => {
81
- try {
82
- const key = attrs && (attrs.id || attrs.href || attrs.src);
83
- if (key && document.querySelector((attrs.id ? ('#' + attrs.id) : (attrs.href ? ('link[href="' + attrs.href + '"]') : ('script[src="' + attrs.src + '"]'))))) {
84
- resolve();
85
- return;
86
- }
87
- const el = document.createElement(tag);
88
- Object.keys(attrs || {}).forEach((k) => el.setAttribute(k, attrs[k]));
89
- el.onload = () => resolve();
90
- el.onerror = () => reject(new Error('load-failed'));
91
- document.head.appendChild(el);
92
- } catch (e) {
93
- reject(e);
94
- }
95
- });
96
- }
97
-
98
- async function ensureFullCalendar() {
99
- if (window.FullCalendar && window.FullCalendar.Calendar) {
100
- return;
101
- }
102
- await loadOnce('script', {
103
- id: 'onekite-fullcalendar-global',
104
- src: 'https://cdn.jsdelivr.net/npm/fullcalendar@latest/index.global.min.js',
105
- async: 'true'
106
- });
107
- await loadOnce('script', {
108
- id: 'onekite-fullcalendar-locales',
109
- src: 'https://cdn.jsdelivr.net/npm/@fullcalendar/core@latest/locales-all.global.min.js',
110
- async: 'true'
111
- });
112
- }
113
-
114
- async function init() {
115
- const el = document.getElementById(containerId);
116
- if (!el) return;
117
-
118
- await ensureFullCalendar();
119
-
120
- // Basic lightweight tooltip (no dependencies, works on hover + tap)
121
- const tip = document.createElement('div');
122
- tip.className = 'onekite-cal-tooltip';
123
- tip.style.display = 'none';
124
- document.body.appendChild(tip);
125
-
126
- function setTipContent(html) {
127
- tip.innerHTML = html;
128
- }
129
-
130
- function showTipAt(x, y) {
131
- tip.style.left = Math.max(8, x + 12) + 'px';
132
- tip.style.top = Math.max(8, y + 12) + 'px';
133
- tip.style.display = 'block';
134
- }
135
-
136
- function hideTip() {
137
- tip.style.display = 'none';
138
- }
139
-
140
- document.addEventListener('click', (e) => {
141
- // Close when clicking outside an event
142
- if (!e.target || !e.target.closest || !e.target.closest('.fc-event')) {
143
- hideTip();
144
- }
145
- }, { passive: true });
146
-
147
- // Define a 2-week dayGrid view
148
- const calendar = new window.FullCalendar.Calendar(el, {
149
- initialView: 'dayGridTwoWeek',
150
- views: {
151
- dayGridTwoWeek: {
152
- type: 'dayGrid',
153
- duration: { weeks: 2 },
154
- buttonText: '2 semaines',
155
- },
156
- },
157
- locale: 'fr',
158
- firstDay: 1,
159
- height: 'auto',
160
- headerToolbar: {
161
- left: 'prev,next',
162
- center: 'title',
163
- right: '',
164
- },
165
- navLinks: false,
166
- eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
167
- // Force day number to be numeric only (avoid '1 janvier' style labels)
168
- dayCellContent: function(arg) {
169
- try {
170
- const t = String(arg.dayNumberText || '');
171
- const m = t.match(/^\d+/);
172
- return { html: m ? m[0] : t };
173
- } catch (e) {
174
- return { html: String(arg.dayNumberText || '') };
175
- }
176
- },
177
- eventClassNames: function(arg) {
178
- try {
179
- const ev = arg && arg.event;
180
- if (!ev) return ['onekite-singleday'];
181
- if (ev.extendedProps && (ev.extendedProps.multiDay === true || ev.extendedProps.days > 1)) return ['onekite-multiday'];
182
- if (ev.start && ev.end) {
183
- const diff = (new Date(ev.end)).getTime() - (new Date(ev.start)).getTime();
184
- return [diff > 86400000 ? 'onekite-multiday' : 'onekite-singleday'];
185
- }
186
- } catch (e) {}
187
- return ['onekite-singleday'];
188
- },
189
- // Render: dot for single-day, pill for multi-day
190
- eventContent: function(arg) {
191
- const ev = arg && arg.event;
192
- const isMulti = (function(){
193
- try {
194
- if (!ev || !ev.start || !ev.end) return false;
195
- if (ev.extendedProps && (ev.extendedProps.multiDay === true || ev.extendedProps.days > 1)) return true;
196
- const s = new Date(ev.start);
197
- const e = new Date(ev.end);
198
- const diff = e.getTime() - s.getTime();
199
- return diff > 86400000; // strictly more than 1 day
200
- } catch (e) { return false; }
201
- })();
202
-
203
- if (isMulti) {
204
- const spacer = document.createElement('span');
205
- spacer.className = 'onekite-pill-spacer';
206
- spacer.appendChild(document.createTextNode('\u00A0'));
207
- return { domNodes: [spacer] };
208
- }
209
-
210
- const bg = (ev.backgroundColor || (ev.extendedProps && ev.extendedProps.backgroundColor) || '').trim();
211
- const border = (ev.borderColor || '').trim();
212
- const color = bg || border || '#3788d8';
213
- const wrap = document.createElement('span');
214
- wrap.className = 'onekite-dot-wrap';
215
- const dot = document.createElement('span');
216
- dot.className = 'onekite-dot';
217
- dot.style.backgroundColor = color;
218
- wrap.appendChild(dot);
219
- return { domNodes: [wrap] };
220
- },
221
- events: function(info, successCallback, failureCallback) {
222
- const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
223
- fetch(eventsEndpoint + '?' + qs.toString(), { credentials: 'same-origin' })
224
- .then((r) => r.json())
225
- .then((json) => successCallback(json || []))
226
- .catch((e) => failureCallback(e));
227
- },
228
- dateClick: function() { window.location.href = calUrl; },
229
- eventDidMount: function(info) {
230
- try {
231
- const ev = info.event;
232
- const ep = ev.extendedProps || {};
233
- const title = (ep.itemNameLine || ep.title || ev.title || '').toString();
234
- const status = (ep.status || ep.type || '').toString();
235
- const start = ev.start ? new Date(ev.start) : null;
236
- const end = ev.end ? new Date(ev.end) : null;
237
- const pad2 = (n) => String(n).padStart(2, '0');
238
- const fmt = (d) => d ? (pad2(d.getDate()) + '/' + pad2(d.getMonth() + 1) + '/' + String(d.getFullYear()).slice(-2)) : '';
239
- const range = (start && end) ? ('Du ' + fmt(start) + ' au ' + fmt(end)) : '';
240
- const html = '' +
241
- '<div style="font-weight:600; margin-bottom:2px;">' + escapeHtml(title) + '</div>' +
242
- (range ? ('<div style="opacity:.85">' + escapeHtml(range) + '</div>') : '') +
243
- (status ? ('<div style="opacity:.75; margin-top:2px; font-size:.85em;">' + escapeHtml(status) + '</div>') : '');
244
-
245
- // Hover (desktop)
246
- info.el.addEventListener('mouseenter', (e) => {
247
- setTipContent(html);
248
- const rect = info.el.getBoundingClientRect();
249
- showTipAt(rect.left + window.scrollX, rect.top + window.scrollY);
250
- }, { passive: true });
251
- info.el.addEventListener('mouseleave', hideTip, { passive: true });
252
-
253
- // Tap/click (mobile)
254
- info.el.addEventListener('click', (e) => {
255
- e.preventDefault();
256
- e.stopPropagation();
257
- setTipContent(html);
258
- const pt = (e.touches && e.touches[0]) ? e.touches[0] : e;
259
- showTipAt((pt.clientX || 0) + window.scrollX, (pt.clientY || 0) + window.scrollY);
260
- });
261
- } catch (e) {}
262
- },
263
- });
264
-
265
- calendar.render();
266
-
267
- // Mobile swipe (left/right) to navigate weeks
268
- try {
269
- let touchStartX = null;
270
- let touchStartY = null;
271
- el.addEventListener('touchstart', (e) => {
272
- const t = e.touches && e.touches[0];
273
- if (!t) return;
274
- touchStartX = t.clientX;
275
- touchStartY = t.clientY;
276
- }, { passive: true });
277
- el.addEventListener('touchend', (e) => {
278
- const t = e.changedTouches && e.changedTouches[0];
279
- if (!t || touchStartX === null || touchStartY === null) return;
280
- const dx = t.clientX - touchStartX;
281
- const dy = t.clientY - touchStartY;
282
- touchStartX = null;
283
- touchStartY = null;
284
- if (Math.abs(dx) < 55) return;
285
- if (Math.abs(dx) < Math.abs(dy) * 1.2) return; // mostly vertical
286
- if (dx < 0) {
287
- calendar.next();
288
- } else {
289
- calendar.prev();
290
- }
291
- }, { passive: true });
292
- } catch (e) {}
293
- }
294
-
295
- // Widgets can be rendered after ajaxify; delay a tick.
296
- setTimeout(() => init().catch(() => {}), 0);
297
- })();
298
- </script>
299
-
300
- <style>
301
- .onekite-twoweeks .fc .fc-toolbar-title { font-size: 1rem; }
302
- .onekite-twoweeks .fc .fc-button { padding: .2rem .35rem; font-size: .75rem; }
303
- .onekite-twoweeks .fc .fc-daygrid-day-number { font-size: .75rem; padding: 2px; }
304
- .onekite-twoweeks .fc .fc-col-header-cell-cushion { font-size: .75rem; }
305
-
306
- /* Single-day: show only a dot */
307
- .onekite-twoweeks .fc .fc-event.onekite-singleday { background: transparent !important; border: none !important; }
308
- .onekite-twoweeks .fc .fc-daygrid-event.onekite-singleday,
309
- .onekite-twoweeks .fc .fc-daygrid-block-event.onekite-singleday {
310
- background: transparent !important;
311
- border: none !important;
312
- box-shadow: none !important;
313
- }
314
- .onekite-twoweeks .fc .fc-daygrid-block-event.onekite-singleday .fc-event-main { padding: 0 !important; }
315
-
316
- /* Multi-day: keep FullCalendar bar, but hide text and make it a pill */
317
- .onekite-twoweeks .fc .fc-event.onekite-multiday {
318
- border-radius: 999px !important;
319
- overflow: hidden;
320
- }
321
- .onekite-twoweeks .fc .fc-daygrid-event.onekite-multiday,
322
- .onekite-twoweeks .fc .fc-daygrid-block-event.onekite-multiday {
323
- border-radius: 999px !important;
324
- box-shadow: none !important;
325
- }
326
- .onekite-twoweeks .fc .fc-event.onekite-multiday .fc-event-main { padding: 0 !important; }
327
- .onekite-twoweeks .fc .fc-event.onekite-multiday .fc-event-title,
328
- .onekite-twoweeks .fc .fc-event.onekite-multiday .fc-event-time { display:none !important; }
329
-
330
- .onekite-twoweeks .fc .fc-daygrid-event-harness { margin-top: 2px; }
331
- .onekite-twoweeks .fc .fc-daygrid-event { padding: 0 !important; }
332
- .onekite-twoweeks .fc .fc-event-main { color: inherit; }
333
- .onekite-twoweeks .fc .fc-event-title { display:none; }
334
- .onekite-dot-wrap { display:flex; align-items:center; justify-content:center; width: auto; }
335
- .onekite-dot { width: 10px; height: 10px; border-radius: 999px; display:inline-block; }
336
- .onekite-pill-spacer { display:block; height: 10px; line-height: 10px; }
337
- .onekite-cal-tooltip {
338
- position: absolute;
339
- z-index: 99999;
340
- max-width: 260px;
341
- background: rgba(0,0,0,.92);
342
- color: #fff;
343
- padding: 8px 10px;
344
- border-radius: 10px;
345
- font-size: 0.9rem;
346
- box-shadow: 0 8px 24px rgba(0,0,0,.25);
347
- pointer-events: none;
348
- }
349
- </style>
350
- `;
351
-
352
- data = data || {};
353
- data.html = html;
354
- return data;
355
- };
356
-
357
- module.exports = widgets;