feat: notes de bord et sauvegarde position calendrier

- bouton crayon sur la fenêtre calendrier (MJ) → dialogue de note
- date impériale + date réelle préremplies
- append au journal configuré (créé auto si inexistant)
- appendJournalNote : labels passés depuis l'ouverture (pas de re-lecture)
- échappement XSS via jQuery .text().html()
- onSave async + await + try/catch
- sauvegarde position calendrier (client scope, debounced 300ms)
- restore position au constructeur
- close() async avec await settings.set
- boutons note et config groupés dans .mgt2-cal-actions (flex row)
This commit is contained in:
2026-07-25 17:08:54 +02:00
parent 8882cf8ee2
commit 47442be3ef
7 changed files with 282 additions and 6 deletions
+35 -2
View File
@@ -22,10 +22,16 @@ export class CalendarApp extends HandlebarsApplicationMixin(ApplicationV2) {
}, },
}; };
constructor({ onConfig, onClose } = {}) { constructor({ onConfig, onClose, onNote } = {}) {
super(); super();
const saved = game.settings?.get(MODULE_ID, 'calendarPosition');
if (saved?.left != null && saved?.top != null) {
this.position.left = saved.left;
this.position.top = saved.top;
}
this._onConfig = onConfig; this._onConfig = onConfig;
this._onClose = onClose; this._onClose = onClose;
this._onNote = onNote;
this._data = { year: 1116, day: 1, hour: 12, minute: 0, planet: '' }; this._data = { year: 1116, day: 1, hour: 12, minute: 0, planet: '' };
} }
@@ -46,6 +52,26 @@ export class CalendarApp extends HandlebarsApplicationMixin(ApplicationV2) {
html.find('.mgt2-cal-config-btn').on('click', () => { html.find('.mgt2-cal-config-btn').on('click', () => {
this._onConfig?.(); this._onConfig?.();
}); });
html.find('.mgt2-cal-note-btn').on('click', () => {
this._onNote?.();
});
}
setPosition(position) {
const result = super.setPosition(position);
this._debouncedSavePos();
return result;
}
_debouncedSavePos() {
if (this._savePosTimeout) clearTimeout(this._savePosTimeout);
this._savePosTimeout = setTimeout(() => {
game.settings?.set(MODULE_ID, 'calendarPosition', {
left: this.position.left,
top: this.position.top,
});
this._savePosTimeout = null;
}, 300);
} }
updateDisplay(data) { updateDisplay(data) {
@@ -53,8 +79,15 @@ export class CalendarApp extends HandlebarsApplicationMixin(ApplicationV2) {
this.render(); this.render();
} }
close() { async close() {
if (game.settings) {
await game.settings.set(MODULE_ID, 'calendarPosition', {
left: this.position.left,
top: this.position.top,
});
}
this._onConfig = null; this._onConfig = null;
this._onNote = null;
this._onClose?.(); this._onClose?.();
this._onClose = null; this._onClose = null;
return super.close(); return super.close();
+59
View File
@@ -0,0 +1,59 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
export class JournalNoteDialog extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
id: 'mgt2-journal-note',
classes: ['mgt2-calendar'],
position: { width: 320, height: 'auto' },
window: {
icon: 'fas fa-pen',
title: 'Note de bord',
resizable: false,
},
};
static PARTS = {
main: {
template: `modules/${MODULE_ID}/templates/journal-note-dialog.hbs`,
root: true,
},
};
constructor({ imperialDate, realDate, onSave } = {}) {
super();
this._imperialDate = imperialDate;
this._realDate = realDate;
this._onSave = onSave;
}
get title() {
return 'Note de bord';
}
async _prepareContext() {
return {
imperialDate: this._imperialDate,
realDate: this._realDate,
};
}
async _onRender(context, options) {
await super._onRender(context, options);
const html = $(this.element);
html.find('[data-action="save"]').on('click', async () => {
const content = html.find('[name="content"]').val()?.trim();
try {
await this._onSave?.(content);
} catch (err) {
console.error(`${MODULE_ID} | Erreur sauvegarde note:`, err);
}
});
}
close() {
this._onSave = null;
return super.close();
}
}
+10
View File
@@ -93,3 +93,13 @@ export function formatImperialDate({ year, day, hour, minute, planet } = {}) {
export function getDefaultCalendar() { export function getDefaultCalendar() {
return { year: 1116, day: 1, hour: 12, minute: 0, planet: '' }; return { year: 1116, day: 1, hour: 12, minute: 0, planet: '' };
} }
export function formatRealDate() {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const h = String(d.getHours()).padStart(2, '0');
const min = String(d.getMinutes()).padStart(2, '0');
return `${day}/${m}/${y} ${h}:${min}`;
}
+67 -1
View File
@@ -1,6 +1,7 @@
import { CalendarApp } from './CalendarApp.js'; import { CalendarApp } from './CalendarApp.js';
import { CalendarConfigDialog } from './CalendarConfigDialog.js'; import { CalendarConfigDialog } from './CalendarConfigDialog.js';
import { getDefaultCalendar } from './calendarDate.js'; import { JournalNoteDialog } from './JournalNoteDialog.js';
import { getDefaultCalendar, formatImperialDate, formatRealDate } from './calendarDate.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov'; const MODULE_ID = 'mgt2-compendium-amiral-denisov';
const ChatLogV2 = foundry.applications.sidebar.tabs.ChatLog; const ChatLogV2 = foundry.applications.sidebar.tabs.ChatLog;
@@ -47,6 +48,7 @@ function openCalendarApp() {
const data = readCalendar(); const data = readCalendar();
_calendarApp = new CalendarApp({ _calendarApp = new CalendarApp({
onConfig: () => openCalendarConfig(), onConfig: () => openCalendarConfig(),
onNote: () => openJournalNote(),
onClose: () => { _calendarApp = null; }, onClose: () => { _calendarApp = null; },
}); });
_calendarApp.updateDisplay(data); _calendarApp.updateDisplay(data);
@@ -67,6 +69,55 @@ function openCalendarConfig() {
dialog.render({ force: true }); dialog.render({ force: true });
} }
async function appendJournalNote(content, { dateLabel, realLabel } = {}) {
if (!content) return false;
const journalName = game.settings.get(MODULE_ID, 'calendarJournalName') || 'Journal de bord';
let journal = game.journal.getName(journalName);
if (!journal) {
journal = await JournalEntry.create({ name: journalName });
if (!journal) {
ui.notifications.error('Impossible de créer le journal');
return false;
}
}
if (!dateLabel || !realLabel) {
const c = readCalendar();
dateLabel = formatImperialDate(c);
realLabel = formatRealDate();
}
const safeContent = $('<div>').text(content).html().replace(/\n/g, '<br>');
const noteHtml = `<hr>
<h3>${realLabel}${dateLabel}</h3>
<p>${safeContent}</p>`;
let page = journal.pages.contents[0];
if (!page) {
[page] = await journal.createEmbeddedDocuments('JournalEntryPage', [{ name: 'Notes', text: { content: '' } }]);
}
if (!page) {
ui.notifications.error('Impossible de créer une page dans le journal');
return false;
}
const existing = page.text?.content || '';
await page.update({ 'text.content': existing + '\n' + noteHtml });
ui.notifications.info(`Note ajoutée au journal « ${journalName} »`);
return true;
}
function openJournalNote() {
const calendar = readCalendar();
const dateLabel = formatImperialDate(calendar);
const realLabel = formatRealDate();
const dialog = new JournalNoteDialog({
imperialDate: dateLabel,
realDate: realLabel,
onSave: async (content) => {
const ok = await appendJournalNote(content, { dateLabel, realLabel });
if (ok) dialog.close();
},
});
dialog.render({ force: true });
}
function registerCalCommand() { function registerCalCommand() {
if (!ChatLogV2?.CHAT_COMMANDS) return; if (!ChatLogV2?.CHAT_COMMANDS) return;
ChatLogV2.CHAT_COMMANDS.cal = { ChatLogV2.CHAT_COMMANDS.cal = {
@@ -125,12 +176,27 @@ Hooks.once('init', () => {
type: Boolean, type: Boolean,
default: true, default: true,
}); });
game.settings.register(MODULE_ID, 'calendarJournalName', {
name: 'Journal de bord',
hint: 'Nom du journal utilisé pour les notes de session. Créé automatiquement s\'il n\'existe pas.',
scope: 'world',
config: true,
type: String,
default: 'Journal de bord',
});
game.settings.register(MODULE_ID, 'calendarPosition', {
scope: 'client',
config: false,
type: Object,
default: {},
});
const loadTemplatesFn = foundry.applications?.handlebars?.loadTemplates || loadTemplates; const loadTemplatesFn = foundry.applications?.handlebars?.loadTemplates || loadTemplates;
if (loadTemplatesFn) { if (loadTemplatesFn) {
loadTemplatesFn([ loadTemplatesFn([
`modules/${MODULE_ID}/templates/calendar-app.hbs`, `modules/${MODULE_ID}/templates/calendar-app.hbs`,
`modules/${MODULE_ID}/templates/calendar-config.hbs`, `modules/${MODULE_ID}/templates/calendar-config.hbs`,
`modules/${MODULE_ID}/templates/journal-note-dialog.hbs`,
]); ]);
} }
+84
View File
@@ -1193,6 +1193,14 @@ a.mgt2-world-link:hover {
flex: 1; flex: 1;
} }
.mgt2-cal-actions {
display: flex;
flex-direction: row;
align-items: center;
gap: 2px;
flex: 0 0 auto;
}
.mgt2-cal-date { .mgt2-cal-date {
color: #d8c79a; color: #d8c79a;
font-size: 0.95em; font-size: 0.95em;
@@ -1352,3 +1360,79 @@ a.mgt2-world-link:hover {
.mgt2-cal-conv-label { .mgt2-cal-conv-label {
color: #8a7a5a; color: #8a7a5a;
} }
/* Note button on calendar app */
.mgt2-cal-note-btn {
flex: 0 0 auto;
width: 22px;
height: 22px;
padding: 0;
border: none;
background: transparent;
color: #8a7a5a;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85em;
border-radius: 3px;
transition: color 0.15s, background 0.15s;
}
.mgt2-cal-note-btn:hover {
color: #d9b24c;
background: rgba(201, 162, 39, 0.15);
}
/* Note dialog */
.mgt2-note-form {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
background: #1a1a2e;
color: #d8c79a;
}
.mgt2-note-row {
display: flex;
flex-direction: column;
gap: 2px;
}
.mgt2-note-row label {
font-size: 0.8em;
color: #8a7a5a;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.mgt2-note-value {
font-size: 0.9em;
color: #d8c79a;
}
.mgt2-note-row textarea {
width: 100%;
padding: 6px;
border: 1px solid #3a3a4e;
border-radius: 3px;
background: #252540;
color: #d8c79a;
font-size: 0.85em;
font-family: inherit;
resize: vertical;
}
.mgt2-note-row textarea:focus {
outline: none;
border-color: #c9a227;
}
.mgt2-note-actions {
display: flex;
justify-content: flex-end;
margin-top: 4px;
}
+5
View File
@@ -3,8 +3,13 @@
<span class="mgt2-cal-date">{{dateLabel}}</span> <span class="mgt2-cal-date">{{dateLabel}}</span>
</div> </div>
{{#if isGM}} {{#if isGM}}
<div class="mgt2-cal-actions">
<button type="button" class="mgt2-cal-note-btn" title="Ajouter une note de bord">
<i class="fas fa-pen"></i>
</button>
<button type="button" class="mgt2-cal-config-btn" title="Configurer le calendrier"> <button type="button" class="mgt2-cal-config-btn" title="Configurer le calendrier">
<i class="fas fa-cog"></i> <i class="fas fa-cog"></i>
</button> </button>
</div>
{{/if}} {{/if}}
</div> </div>
+19
View File
@@ -0,0 +1,19 @@
<form class="mgt2-note-form">
<div class="mgt2-note-row">
<label>Date impériale</label>
<span class="mgt2-note-value">{{imperialDate}}</span>
</div>
<div class="mgt2-note-row">
<label>Date réelle</label>
<span class="mgt2-note-value">{{realDate}}</span>
</div>
<div class="mgt2-note-row">
<label for="note-content">Note</label>
<textarea id="note-content" name="content" rows="6" placeholder="Saisissez votre note…" autofocus></textarea>
</div>
<div class="mgt2-note-actions">
<button type="button" class="mgt2-cal-btn mgt2-cal-btn-primary" data-action="save">
<i class="fas fa-save"></i> Enregistrer
</button>
</div>
</form>