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:
+35
-2
@@ -22,10 +22,16 @@ export class CalendarApp extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
},
|
||||
};
|
||||
|
||||
constructor({ onConfig, onClose } = {}) {
|
||||
constructor({ onConfig, onClose, onNote } = {}) {
|
||||
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._onClose = onClose;
|
||||
this._onNote = onNote;
|
||||
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', () => {
|
||||
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) {
|
||||
@@ -53,8 +79,15 @@ export class CalendarApp extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
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._onNote = null;
|
||||
this._onClose?.();
|
||||
this._onClose = null;
|
||||
return super.close();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -93,3 +93,13 @@ export function formatImperialDate({ year, day, hour, minute, planet } = {}) {
|
||||
export function getDefaultCalendar() {
|
||||
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}`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CalendarApp } from './CalendarApp.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 ChatLogV2 = foundry.applications.sidebar.tabs.ChatLog;
|
||||
@@ -47,6 +48,7 @@ function openCalendarApp() {
|
||||
const data = readCalendar();
|
||||
_calendarApp = new CalendarApp({
|
||||
onConfig: () => openCalendarConfig(),
|
||||
onNote: () => openJournalNote(),
|
||||
onClose: () => { _calendarApp = null; },
|
||||
});
|
||||
_calendarApp.updateDisplay(data);
|
||||
@@ -67,6 +69,55 @@ function openCalendarConfig() {
|
||||
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() {
|
||||
if (!ChatLogV2?.CHAT_COMMANDS) return;
|
||||
ChatLogV2.CHAT_COMMANDS.cal = {
|
||||
@@ -125,12 +176,27 @@ Hooks.once('init', () => {
|
||||
type: Boolean,
|
||||
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;
|
||||
if (loadTemplatesFn) {
|
||||
loadTemplatesFn([
|
||||
`modules/${MODULE_ID}/templates/calendar-app.hbs`,
|
||||
`modules/${MODULE_ID}/templates/calendar-config.hbs`,
|
||||
`modules/${MODULE_ID}/templates/journal-note-dialog.hbs`,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1193,6 +1193,14 @@ a.mgt2-world-link:hover {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mgt2-cal-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mgt2-cal-date {
|
||||
color: #d8c79a;
|
||||
font-size: 0.95em;
|
||||
@@ -1352,3 +1360,79 @@ a.mgt2-world-link:hover {
|
||||
.mgt2-cal-conv-label {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
<span class="mgt2-cal-date">{{dateLabel}}</span>
|
||||
</div>
|
||||
{{#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">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user