@forcecalendar/interface 1.0.27 → 1.0.28
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/README.md +9 -0
- package/dist/force-calendar-interface.esm.js +102 -55
- package/dist/force-calendar-interface.esm.js.map +1 -1
- package/dist/force-calendar-interface.umd.js.map +1 -1
- package/package.json +3 -1
- package/src/components/EventForm.js +180 -176
- package/src/components/ForceCalendar.js +414 -392
- package/src/core/BaseComponent.js +146 -144
- package/src/core/EventBus.js +197 -197
- package/src/core/StateManager.js +405 -399
- package/src/index.js +3 -3
- package/src/renderers/BaseViewRenderer.js +195 -192
- package/src/renderers/DayViewRenderer.js +133 -118
- package/src/renderers/MonthViewRenderer.js +74 -72
- package/src/renderers/WeekViewRenderer.js +118 -96
- package/src/utils/DOMUtils.js +277 -277
- package/src/utils/DateUtils.js +164 -164
- package/src/utils/StyleUtils.js +286 -249
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forcecalendar/interface",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Official interface layer for forceCalendar Core - Enterprise calendar components",
|
|
6
6
|
"main": "dist/force-calendar-interface.umd.js",
|
|
@@ -50,8 +50,10 @@
|
|
|
50
50
|
"@babel/core": "^7.28.5",
|
|
51
51
|
"@babel/preset-env": "^7.28.5",
|
|
52
52
|
"babel-jest": "^30.2.0",
|
|
53
|
+
"eslint": "^8.57.1",
|
|
53
54
|
"jest": "^30.2.0",
|
|
54
55
|
"jest-environment-jsdom": "^30.2.0",
|
|
56
|
+
"prettier": "^3.8.1",
|
|
55
57
|
"vite": "^5.0.0"
|
|
56
58
|
}
|
|
57
59
|
}
|
|
@@ -3,47 +3,47 @@ import { StyleUtils } from '../utils/StyleUtils.js';
|
|
|
3
3
|
import { DOMUtils } from '../utils/DOMUtils.js';
|
|
4
4
|
|
|
5
5
|
export class EventForm extends BaseComponent {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this._isVisible = false;
|
|
9
|
+
this._cleanupFocusTrap = null;
|
|
10
|
+
this.config = {
|
|
11
|
+
title: 'New Event',
|
|
12
|
+
defaultDuration: 60, // minutes
|
|
13
|
+
colors: [
|
|
14
|
+
{ color: '#2563EB', label: 'Blue' },
|
|
15
|
+
{ color: '#10B981', label: 'Green' },
|
|
16
|
+
{ color: '#F59E0B', label: 'Amber' },
|
|
17
|
+
{ color: '#EF4444', label: 'Red' },
|
|
18
|
+
{ color: '#8B5CF6', label: 'Purple' },
|
|
19
|
+
{ color: '#6B7280', label: 'Gray' }
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
this._formData = {
|
|
23
|
+
title: '',
|
|
24
|
+
start: new Date(),
|
|
25
|
+
end: new Date(),
|
|
26
|
+
allDay: false,
|
|
27
|
+
color: this.config.colors[0].color
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static get observedAttributes() {
|
|
32
|
+
return ['open'];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
36
|
+
if (name === 'open') {
|
|
37
|
+
if (newValue !== null) {
|
|
38
|
+
this.open();
|
|
39
|
+
} else {
|
|
40
|
+
this.close();
|
|
41
|
+
}
|
|
43
42
|
}
|
|
43
|
+
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
getStyles() {
|
|
46
|
+
return `
|
|
47
47
|
${StyleUtils.getBaseStyles()}
|
|
48
48
|
${StyleUtils.getButtonStyles()}
|
|
49
49
|
|
|
@@ -214,10 +214,10 @@ export class EventForm extends BaseComponent {
|
|
|
214
214
|
border-color: var(--fc-danger-color);
|
|
215
215
|
}
|
|
216
216
|
`;
|
|
217
|
-
|
|
217
|
+
}
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
template() {
|
|
220
|
+
return `
|
|
221
221
|
<div class="modal-content" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
|
222
222
|
<header class="modal-header">
|
|
223
223
|
<h3 class="modal-title" id="modal-title">${this.config.title}</h3>
|
|
@@ -250,7 +250,9 @@ export class EventForm extends BaseComponent {
|
|
|
250
250
|
<div class="form-group">
|
|
251
251
|
<label id="color-label">Color</label>
|
|
252
252
|
<div class="color-options" id="color-picker" role="radiogroup" aria-labelledby="color-label">
|
|
253
|
-
${this.config.colors
|
|
253
|
+
${this.config.colors
|
|
254
|
+
.map(
|
|
255
|
+
c => `
|
|
254
256
|
<button type="button"
|
|
255
257
|
class="color-btn ${c.color === this._formData.color ? 'selected' : ''}"
|
|
256
258
|
style="background-color: ${c.color}"
|
|
@@ -259,7 +261,9 @@ export class EventForm extends BaseComponent {
|
|
|
259
261
|
aria-label="${c.label}"
|
|
260
262
|
aria-checked="${c.color === this._formData.color ? 'true' : 'false'}"
|
|
261
263
|
role="radio"></button>
|
|
262
|
-
`
|
|
264
|
+
`
|
|
265
|
+
)
|
|
266
|
+
.join('')}
|
|
263
267
|
</div>
|
|
264
268
|
</div>
|
|
265
269
|
</div>
|
|
@@ -270,155 +274,155 @@ export class EventForm extends BaseComponent {
|
|
|
270
274
|
</footer>
|
|
271
275
|
</div>
|
|
272
276
|
`;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
};
|
|
310
|
-
window.addEventListener('keydown', this._handleKeyDown);
|
|
311
|
-
this._keydownListenerAdded = true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
afterRender() {
|
|
280
|
+
// Bind elements
|
|
281
|
+
this.modalContent = this.$('.modal-content');
|
|
282
|
+
this.titleInput = this.$('#event-title');
|
|
283
|
+
this.startInput = this.$('#event-start');
|
|
284
|
+
this.endInput = this.$('#event-end');
|
|
285
|
+
this.colorContainer = this.$('#color-picker');
|
|
286
|
+
|
|
287
|
+
this.titleGroup = this.$('#title-group');
|
|
288
|
+
this.endGroup = this.$('#end-group');
|
|
289
|
+
|
|
290
|
+
// Event Listeners using addListener for automatic cleanup
|
|
291
|
+
this.addListener(this.$('#close-x'), 'click', () => this.close());
|
|
292
|
+
this.addListener(this.$('#cancel-btn'), 'click', () => this.close());
|
|
293
|
+
this.addListener(this.$('#save-btn'), 'click', () => this.save());
|
|
294
|
+
|
|
295
|
+
this.colorContainer.querySelectorAll('.color-btn').forEach(btn => {
|
|
296
|
+
this.addListener(btn, 'click', e => {
|
|
297
|
+
this._formData.color = e.currentTarget.dataset.color;
|
|
298
|
+
this.updateColorSelection();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Close on backdrop click
|
|
303
|
+
this.addListener(this, 'click', e => {
|
|
304
|
+
if (e.target === this) this.close();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Close on Escape key - only add once to prevent memory leaks
|
|
308
|
+
if (!this._keydownListenerAdded) {
|
|
309
|
+
this._handleKeyDown = e => {
|
|
310
|
+
if (e.key === 'Escape' && this.hasAttribute('open')) {
|
|
311
|
+
this.close();
|
|
312
312
|
}
|
|
313
|
+
};
|
|
314
|
+
window.addEventListener('keydown', this._handleKeyDown);
|
|
315
|
+
this._keydownListenerAdded = true;
|
|
313
316
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
updateColorSelection() {
|
|
320
|
+
const buttons = this.colorContainer.querySelectorAll('.color-btn');
|
|
321
|
+
buttons.forEach(btn => {
|
|
322
|
+
const isSelected = btn.dataset.color === this._formData.color;
|
|
323
|
+
btn.classList.toggle('selected', isSelected);
|
|
324
|
+
btn.setAttribute('aria-checked', isSelected ? 'true' : 'false');
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
open(initialDate = new Date()) {
|
|
329
|
+
if (!this.hasAttribute('open')) {
|
|
330
|
+
this.setAttribute('open', '');
|
|
322
331
|
}
|
|
323
332
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
this.endInput.value = this.formatDateForInput(this._formData.end);
|
|
344
|
-
this.updateColorSelection();
|
|
345
|
-
|
|
346
|
-
// Focus trapping
|
|
347
|
-
this._cleanupFocusTrap = DOMUtils.trapFocus(this.modalContent);
|
|
348
|
-
}
|
|
333
|
+
// Reset errors
|
|
334
|
+
this.titleGroup.classList.remove('has-error');
|
|
335
|
+
this.endGroup.classList.remove('has-error');
|
|
336
|
+
|
|
337
|
+
// Initialize form data
|
|
338
|
+
this._formData.start = initialDate;
|
|
339
|
+
this._formData.end = new Date(initialDate.getTime() + this.config.defaultDuration * 60 * 1000);
|
|
340
|
+
this._formData.title = '';
|
|
341
|
+
this._formData.color = this.config.colors[0].color;
|
|
342
|
+
|
|
343
|
+
// Update inputs
|
|
344
|
+
if (this.startInput) {
|
|
345
|
+
this.titleInput.value = '';
|
|
346
|
+
this.startInput.value = this.formatDateForInput(this._formData.start);
|
|
347
|
+
this.endInput.value = this.formatDateForInput(this._formData.end);
|
|
348
|
+
this.updateColorSelection();
|
|
349
|
+
|
|
350
|
+
// Focus trapping
|
|
351
|
+
this._cleanupFocusTrap = DOMUtils.trapFocus(this.modalContent);
|
|
349
352
|
}
|
|
353
|
+
}
|
|
350
354
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
355
|
+
close() {
|
|
356
|
+
this.removeAttribute('open');
|
|
357
|
+
if (this._cleanupFocusTrap) {
|
|
358
|
+
this._cleanupFocusTrap();
|
|
359
|
+
this._cleanupFocusTrap = null;
|
|
357
360
|
}
|
|
361
|
+
}
|
|
358
362
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// Reset errors
|
|
363
|
-
this.titleGroup.classList.remove('has-error');
|
|
364
|
-
this.endGroup.classList.remove('has-error');
|
|
365
|
-
|
|
366
|
-
// Check title
|
|
367
|
-
if (!this.titleInput.value.trim()) {
|
|
368
|
-
this.titleGroup.classList.add('has-error');
|
|
369
|
-
isValid = false;
|
|
370
|
-
}
|
|
363
|
+
validate() {
|
|
364
|
+
let isValid = true;
|
|
371
365
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (end <= start) {
|
|
376
|
-
this.endGroup.classList.add('has-error');
|
|
377
|
-
isValid = false;
|
|
378
|
-
}
|
|
366
|
+
// Reset errors
|
|
367
|
+
this.titleGroup.classList.remove('has-error');
|
|
368
|
+
this.endGroup.classList.remove('has-error');
|
|
379
369
|
|
|
380
|
-
|
|
370
|
+
// Check title
|
|
371
|
+
if (!this.titleInput.value.trim()) {
|
|
372
|
+
this.titleGroup.classList.add('has-error');
|
|
373
|
+
isValid = false;
|
|
381
374
|
}
|
|
382
375
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
end: new Date(this.endInput.value),
|
|
390
|
-
backgroundColor: this._formData.color
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
this.emit('save', event);
|
|
394
|
-
this.close();
|
|
376
|
+
// Check date range
|
|
377
|
+
const start = new Date(this.startInput.value);
|
|
378
|
+
const end = new Date(this.endInput.value);
|
|
379
|
+
if (end <= start) {
|
|
380
|
+
this.endGroup.classList.add('has-error');
|
|
381
|
+
isValid = false;
|
|
395
382
|
}
|
|
396
383
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
384
|
+
return isValid;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
save() {
|
|
388
|
+
if (!this.validate()) return;
|
|
389
|
+
|
|
390
|
+
const event = {
|
|
391
|
+
title: this.titleInput.value.trim(),
|
|
392
|
+
start: new Date(this.startInput.value),
|
|
393
|
+
end: new Date(this.endInput.value),
|
|
394
|
+
backgroundColor: this._formData.color
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
this.emit('save', event);
|
|
398
|
+
this.close();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
formatDateForInput(date) {
|
|
402
|
+
// Handle local date string for datetime-local input
|
|
403
|
+
const pad = num => String(num).padStart(2, '0');
|
|
404
|
+
const year = date.getFullYear();
|
|
405
|
+
const month = pad(date.getMonth() + 1);
|
|
406
|
+
const day = pad(date.getDate());
|
|
407
|
+
const hours = pad(date.getHours());
|
|
408
|
+
const minutes = pad(date.getMinutes());
|
|
409
|
+
|
|
410
|
+
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
unmount() {
|
|
414
|
+
if (this._cleanupFocusTrap) {
|
|
415
|
+
this._cleanupFocusTrap();
|
|
407
416
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
// Clean up window listener
|
|
414
|
-
if (this._handleKeyDown) {
|
|
415
|
-
window.removeEventListener('keydown', this._handleKeyDown);
|
|
416
|
-
this._handleKeyDown = null;
|
|
417
|
-
this._keydownListenerAdded = false;
|
|
418
|
-
}
|
|
417
|
+
// Clean up window listener
|
|
418
|
+
if (this._handleKeyDown) {
|
|
419
|
+
window.removeEventListener('keydown', this._handleKeyDown);
|
|
420
|
+
this._handleKeyDown = null;
|
|
421
|
+
this._keydownListenerAdded = false;
|
|
419
422
|
}
|
|
423
|
+
}
|
|
420
424
|
}
|
|
421
425
|
|
|
422
426
|
if (!customElements.get('forcecal-event-form')) {
|
|
423
|
-
|
|
427
|
+
customElements.define('forcecal-event-form', EventForm);
|
|
424
428
|
}
|