From f7eff2da3bf95cd957152f6485bb063e9c2769bf Mon Sep 17 00:00:00 2001 From: Dean Vigoren Date: Mon, 25 Sep 2023 16:36:01 -0600 Subject: [PATCH 1/5] Fixed a bug where selecting a date range across months would cause the dialog to close before being able to select the second date. Updated the close event for date selectors to clean up after itself when the dialog it is in is closed. Updated the unit tests. --- CHANGELOG.md | 11 + src/classes/applications/configuration-app.ts | 1 + .../date-selector-manager.test.ts | 63 +++-- .../date-selector/date-selector-manager.ts | 7 + src/classes/date-selector/index.test.ts | 238 ++++++++++-------- src/classes/date-selector/index.ts | 27 +- src/classes/notes/note-sheet.ts | 1 + src/classes/renderer/calendar-full.ts | 2 +- 8 files changed, 210 insertions(+), 140 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c60f72c..6392345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## 2.4.1 - Bug Fixes + +![](https://img.shields.io/badge/release%20date-September%2025%2C%202023-blue) +![GitHub release](https://img.shields.io/github/downloads-pre/vigoren/foundryvtt-simple-calendar/v2.4.1/module.zip) + +### Bug Fixes + +- Fixed an issue where selecting a date range across multiple months would close the select dialog before the second date could be chosen. ([#547](https://github.com/vigoren/foundryvtt-simple-calendar/issues/547)) + +
+ ## 2.4.0 - Leap Year Starting Year and Bug Fixes ![](https://img.shields.io/badge/release%20date-September%2012%2C%202023-blue) diff --git a/src/classes/applications/configuration-app.ts b/src/classes/applications/configuration-app.ts index cdbf0b7..e8b2639 100644 --- a/src/classes/applications/configuration-app.ts +++ b/src/classes/applications/configuration-app.ts @@ -194,6 +194,7 @@ export default class ConfigurationApp extends FormApplication { DateSelectorManager.RemoveSelector(`sc_season_start_date_${s.id}`); DateSelectorManager.RemoveSelector(`sc_season_sunrise_time_${s.id}`); }); + DateSelectorManager.DeactivateSelector("quick-setup-predefined-calendar"); this.appWindow = null; return super.close(options); } diff --git a/src/classes/date-selector/date-selector-manager.test.ts b/src/classes/date-selector/date-selector-manager.test.ts index d9bf0a4..780c622 100644 --- a/src/classes/date-selector/date-selector-manager.test.ts +++ b/src/classes/date-selector/date-selector-manager.test.ts @@ -2,67 +2,76 @@ * @jest-environment jsdom */ import "../../../__mocks__/index"; -import {jest, beforeEach, describe, expect, test} from '@jest/globals'; +import { jest, beforeEach, describe, expect, test } from "@jest/globals"; import DateSelectorManager from "./date-selector-manager"; -import {DateSelector} from "./index"; +import { DateSelector } from "./index"; import Calendar from "../calendar"; -import {CalManager, updateCalManager} from "../index"; +import { CalManager, updateCalManager } from "../index"; import CalendarManager from "../calendar/calendar-manager"; - -describe('Date Selector Manager Class Tests', () => { +describe("Date Selector Manager Class Tests", () => { let tCal: Calendar; - let ds: DateSelector + let ds: DateSelector; - beforeEach(()=>{ + beforeEach(() => { updateCalManager(new CalendarManager()); - tCal = new Calendar('',''); - jest.spyOn(CalManager, 'getActiveCalendar').mockImplementation(() => {return tCal;}); + tCal = new Calendar("", ""); + jest.spyOn(CalManager, "getActiveCalendar").mockImplementation(() => { + return tCal; + }); DateSelectorManager.Selectors = {}; - ds = DateSelectorManager.GetSelector('test', {showDateSelector: true, showTimeSelector: true}); + ds = DateSelectorManager.GetSelector("test", { showDateSelector: true, showTimeSelector: true }); }); - test('Get Selector', () => { - let newDs = DateSelectorManager.GetSelector('test2', { + test("Get Selector", () => { + let newDs = DateSelectorManager.GetSelector("test2", { showDateSelector: true, showTimeSelector: true, allowDateRangeSelection: true, onDateSelect: () => {}, - timeDelimiter: '/', + timeDelimiter: "/", showCalendarYear: false, timeSelected: false, allowTimeRangeSelection: true, - selectedStartDate: {year: 0, month: 1, day: 1, hour: 0, minute: 0, seconds: 0}, - selectedEndDate: {year: 0, month: 1, day: 1, hour: 0, minute: 0, seconds: 0} + selectedStartDate: { year: 0, month: 1, day: 1, hour: 0, minute: 0, seconds: 0 }, + selectedEndDate: { year: 0, month: 1, day: 1, hour: 0, minute: 0, seconds: 0 } }); - expect(newDs.id).toBe('test2'); + expect(newDs.id).toBe("test2"); expect(newDs.allowDateRangeSelection).toBe(true); - expect(newDs.onDateSelect ).not.toBeNull(); + expect(newDs.onDateSelect).not.toBeNull(); expect(Object.keys(DateSelectorManager.Selectors).length).toBe(2); - newDs = DateSelectorManager.GetSelector('test', { + newDs = DateSelectorManager.GetSelector("test", { showDateSelector: true, showTimeSelector: true, - selectedStartDate: {year: 1, month: 1, day: 1, hour: 0, minute: 0, seconds: 0}, - selectedEndDate: {year: 1, month: 1, day: 1, hour: 0, minute: 0, seconds: 0} + selectedStartDate: { year: 1, month: 1, day: 1, hour: 0, minute: 0, seconds: 0 }, + selectedEndDate: { year: 1, month: 1, day: 1, hour: 0, minute: 0, seconds: 0 } }); expect(newDs).toStrictEqual(ds); }); - test('Remove Selector', () => { + test("Remove Selector", () => { expect(Object.keys(DateSelectorManager.Selectors).length).toBe(1); - DateSelectorManager.RemoveSelector('no'); + DateSelectorManager.RemoveSelector("no"); expect(Object.keys(DateSelectorManager.Selectors).length).toBe(1); - DateSelectorManager.RemoveSelector('test'); + DateSelectorManager.RemoveSelector("test"); expect(Object.keys(DateSelectorManager.Selectors).length).toBe(0); }); - test('Activate Selector', () => { - jest.spyOn(ds, 'activateListeners').mockImplementation(() => {}); - DateSelectorManager.ActivateSelector('no'); + test("Activate Selector", () => { + jest.spyOn(ds, "activateListeners").mockImplementation(() => {}); + DateSelectorManager.ActivateSelector("no"); expect(ds.activateListeners).not.toHaveBeenCalled(); - DateSelectorManager.ActivateSelector('test'); + DateSelectorManager.ActivateSelector("test"); expect(ds.activateListeners).toHaveBeenCalledTimes(1); }); + + test("Deactivate Selector", () => { + jest.spyOn(ds, "deactivateListeners").mockImplementation(() => {}); + DateSelectorManager.DeactivateSelector("no"); + expect(ds.deactivateListeners).not.toHaveBeenCalled(); + DateSelectorManager.DeactivateSelector("test"); + expect(ds.deactivateListeners).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/classes/date-selector/date-selector-manager.ts b/src/classes/date-selector/date-selector-manager.ts index f7fd3ce..1c5a835 100644 --- a/src/classes/date-selector/date-selector-manager.ts +++ b/src/classes/date-selector/date-selector-manager.ts @@ -29,6 +29,7 @@ export default class DateSelectorManager { */ static RemoveSelector(id: string) { if (Object.prototype.hasOwnProperty.call(this.Selectors, id)) { + this.Selectors[id].deactivateListeners(); delete this.Selectors[id]; } } @@ -42,4 +43,10 @@ export default class DateSelectorManager { this.Selectors[id].activateListeners(); } } + + static DeactivateSelector(id: string) { + if (Object.prototype.hasOwnProperty.call(this.Selectors, id)) { + this.Selectors[id].deactivateListeners(); + } + } } diff --git a/src/classes/date-selector/index.test.ts b/src/classes/date-selector/index.test.ts index 74e874b..8bb4b25 100644 --- a/src/classes/date-selector/index.test.ts +++ b/src/classes/date-selector/index.test.ts @@ -2,34 +2,36 @@ * @jest-environment jsdom */ import "../../../__mocks__/index"; -import {jest, beforeEach, describe, expect, test} from '@jest/globals'; +import { jest, beforeEach, describe, expect, test } from "@jest/globals"; -import {DateSelector} from "./index"; -import {CalendarClickEvents, DateSelectorPositions} from "../../constants"; +import { DateSelector } from "./index"; +import { CalendarClickEvents, DateSelectorPositions } from "../../constants"; import Calendar from "../calendar"; -import {CalManager, updateCalManager} from "../index"; +import { CalManager, updateCalManager } from "../index"; import CalendarManager from "../calendar/calendar-manager"; import Renderer from "../renderer"; import * as DateUtils from "../utilities/date-time"; -describe('Date Selector Class Tests', () => { +describe("Date Selector Class Tests", () => { let tCal: Calendar; let ds: DateSelector; beforeEach(() => { updateCalManager(new CalendarManager()); - tCal = new Calendar('',''); - jest.spyOn(CalManager, 'getActiveCalendar').mockImplementation(() => {return tCal;}); - ds = new DateSelector('test', {}); + tCal = new Calendar("", ""); + jest.spyOn(CalManager, "getActiveCalendar").mockImplementation(() => { + return tCal; + }); + ds = new DateSelector("test", {}); }); - test('Constructor', () => { + test("Constructor", () => { expect(ds.addTime).toBe(false); - ds = new DateSelector('test', {timeSelected: true}); - expect(ds.addTime).toBe(true) + ds = new DateSelector("test", { timeSelected: true }); + expect(ds.addTime).toBe(true); }); - test('Apply Options', () => { + test("Apply Options", () => { ds.applyOptions({ calendar: tCal, showDateSelector: true, @@ -40,7 +42,7 @@ describe('Date Selector Class Tests', () => { allowDateRangeSelection: true, allowTimeRangeSelection: true, editYear: true, - timeDelimiter: '~', + timeDelimiter: "~", selectedStartDate: { year: 0, month: 0, @@ -70,10 +72,16 @@ describe('Date Selector Class Tests', () => { expect(ds.useCloneCalendars).toBe(true); }); - test('Build', () => { - jest.spyOn(Renderer.CalendarFull, 'Render').mockImplementation(() => {return '';}); - jest.spyOn(Renderer.TimeSelector, 'Render').mockImplementation(() => {return '';}); - jest.spyOn(DateUtils, 'GetDisplayDate').mockImplementation(() => {return '';}); + test("Build", () => { + jest.spyOn(Renderer.CalendarFull, "Render").mockImplementation(() => { + return ""; + }); + jest.spyOn(Renderer.TimeSelector, "Render").mockImplementation(() => { + return ""; + }); + jest.spyOn(DateUtils, "GetDisplayDate").mockImplementation(() => { + return ""; + }); expect(ds.build()).toBeDefined(); expect(Renderer.CalendarFull.Render).toHaveBeenCalledTimes(1); @@ -99,46 +107,66 @@ describe('Date Selector Class Tests', () => { expect(DateUtils.GetDisplayDate).toHaveBeenCalledTimes(4); }); - test('Set Position', () => { - const elm = document.createElement('div'); + test("Set Position", () => { + const elm = document.createElement("div"); ds.setPosition(elm); expect(elm.classList.contains(DateSelectorPositions.Auto)).toBe(true); - const container = document.createElement('div'); - jest.spyOn(elm,'closest').mockImplementation(() => {return container;}); + const container = document.createElement("div"); + jest.spyOn(elm, "closest").mockImplementation(() => { + return container; + }); - const elmgbcr = jest.spyOn(elm, 'getBoundingClientRect').mockReturnValue({top:0, left:0, right: 0, bottom:0, y: 0, x: 0, width: 0, height:0, toJSON: () => {}}); - jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({top:0, left:0, right: 0, bottom:0, y: 0, x: 0, width: 0, height:0, toJSON: () => {}}); + const elmgbcr = jest + .spyOn(elm, "getBoundingClientRect") + .mockReturnValue({ top: 0, left: 0, right: 0, bottom: 0, y: 0, x: 0, width: 0, height: 0, toJSON: () => {} }); + jest.spyOn(container, "getBoundingClientRect").mockReturnValue({ + top: 0, + left: 0, + right: 0, + bottom: 0, + y: 0, + x: 0, + width: 0, + height: 0, + toJSON: () => {} + }); ds.setPosition(elm); - expect(elm.classList.contains('left-down')).toBe(true); + expect(elm.classList.contains("left-down")).toBe(true); - elmgbcr.mockReturnValue({top:10, left:0, right: 10, bottom:20, y: 0, x: 0, width: 0, height:5, toJSON: () => {}}); + elmgbcr.mockReturnValue({ top: 10, left: 0, right: 10, bottom: 20, y: 0, x: 0, width: 0, height: 5, toJSON: () => {} }); ds.setPosition(elm); - expect(elm.classList.contains('right-up')).toBe(true); + expect(elm.classList.contains("right-up")).toBe(true); }); - test('Update', () => { - jest.spyOn(ds, 'build').mockImplementation(() => {return '';}); - jest.spyOn(ds, 'setPosition').mockImplementation(() => {}); + test("Update", () => { + jest.spyOn(ds, "build").mockImplementation(() => { + return ""; + }); + jest.spyOn(ds, "setPosition").mockImplementation(() => {}); ds.update(); expect(ds.build).toHaveBeenCalledTimes(1); - const docQSSpy = jest.spyOn(document, 'querySelector'); - docQSSpy.mockReturnValue(document.createElement('div')); + const docQSSpy = jest.spyOn(document, "querySelector"); + docQSSpy.mockReturnValue(document.createElement("div")); ds.update(); expect(ds.build).toHaveBeenCalledTimes(2); expect(ds.setPosition).toHaveBeenCalledTimes(1); }); - test('Activate Listeners', () => { + test("Activate Listeners", () => { ds.activateListeners(); - const html = document.createElement('div'); - const htmlChild = document.createElement('div'); - const qs = jest.spyOn(document, 'querySelector').mockImplementation(() => {return html;}); - const htmlQs = jest.spyOn(html, 'querySelector').mockImplementation(() => {return null;}); - jest.spyOn(htmlChild, 'addEventListener').mockImplementation(() => {}); + const html = document.createElement("div"); + const htmlChild = document.createElement("div"); + const qs = jest.spyOn(document, "querySelector").mockImplementation(() => { + return html; + }); + const htmlQs = jest.spyOn(html, "querySelector").mockImplementation(() => { + return null; + }); + jest.spyOn(htmlChild, "addEventListener").mockImplementation(() => {}); ds.activateListeners(); expect(qs).toHaveBeenCalledTimes(1); @@ -153,76 +181,83 @@ describe('Date Selector Class Tests', () => { expect(htmlChild.addEventListener).toHaveBeenCalledTimes(4); }); - test('Call On Date Select', () => { + test("Deactivate Listeners", () => { + const relSpy = jest.spyOn(document, "removeEventListener"); + ds.deactivateListeners(); + expect(relSpy).toHaveBeenCalledTimes(1); + }); + + test("Call On Date Select", () => { ds.onDateSelect = jest.fn(); ds.callOnDateSelect(); expect(ds.onDateSelect).toHaveBeenCalledTimes(1); }); - test('Toggle Calendar', () => { - const fakeWrapper = document.createElement('div'); - const fwqsSpy = jest.spyOn(fakeWrapper, 'querySelector'); - const returnedElement = document.createElement('div'); + test("Toggle Calendar", () => { + const fakeWrapper = document.createElement("div"); + const fwqsSpy = jest.spyOn(fakeWrapper, "querySelector"); + const returnedElement = document.createElement("div"); - ds.toggleCalendar(fakeWrapper, new Event('click')); + ds.toggleCalendar(fakeWrapper, new Event("click")); expect(fwqsSpy).toHaveBeenCalledTimes(1); fwqsSpy.mockReturnValue(returnedElement); - ds.toggleCalendar(fakeWrapper, new Event('click')); + ds.toggleCalendar(fakeWrapper, new Event("click")); expect(fwqsSpy).toHaveBeenCalledTimes(2); - expect(returnedElement.style.display).toBe('none'); + expect(returnedElement.style.display).toBe("none"); - ds.toggleCalendar(fakeWrapper, new Event('click')); + ds.toggleCalendar(fakeWrapper, new Event("click")); expect(fwqsSpy).toHaveBeenCalledTimes(3); - expect(returnedElement.style.display).toBe('block'); + expect(returnedElement.style.display).toBe("block"); }); - test('Hide Calendar', () => { - const fakeWrapper = document.createElement('div'); - const fwqsSpy = jest.spyOn(fakeWrapper, 'querySelector'); - const returnedElement = document.createElement('div'); + test("Hide Calendar", () => { + const fakeWrapper = document.createElement("div"); + const docSpy = jest.spyOn(document, "getElementById").mockReturnValue(fakeWrapper); + const fwqsSpy = jest.spyOn(fakeWrapper, "querySelector"); + const returnedElement = document.createElement("div"); //@ts-ignore - jest.spyOn(returnedElement, 'getClientRects').mockReturnValue([1]); + jest.spyOn(returnedElement, "getClientRects").mockReturnValue([1]); - ds.hideCalendar(fakeWrapper); + ds.hideCalendar(); expect(fwqsSpy).toHaveBeenCalledTimes(1); fwqsSpy.mockReturnValue(returnedElement); - ds.hideCalendar(fakeWrapper); + ds.hideCalendar(); expect(fwqsSpy).toHaveBeenCalledTimes(2); - expect(returnedElement.style.display).toBe('none'); + expect(returnedElement.style.display).toBe("none"); ds.onDateSelect = jest.fn(); - ds.hideCalendar(fakeWrapper); + ds.hideCalendar(); expect(fwqsSpy).toHaveBeenCalledTimes(3); expect(ds.onDateSelect).toHaveBeenCalledTimes(1); }); - test('Calendar Click', () => { - const wrapElm = document.createElement('div'); - const ddElm = document.createElement('div'); - jest.spyOn(Renderer.TimeSelector, 'HideTimeDropdown').mockImplementation(()=>{}); - const gebiSpy = jest.spyOn(document, 'getElementById').mockReturnValue(wrapElm); + test("Calendar Click", () => { + const wrapElm = document.createElement("div"); + const ddElm = document.createElement("div"); + jest.spyOn(Renderer.TimeSelector, "HideTimeDropdown").mockImplementation(() => {}); + const gebiSpy = jest.spyOn(document, "getElementById").mockReturnValue(wrapElm); //@ts-ignore - jest.spyOn(wrapElm, 'getElementsByClassName').mockReturnValue([ddElm]); + jest.spyOn(wrapElm, "getElementsByClassName").mockReturnValue([ddElm]); - ds.calendarClick(new Event('click')); + ds.calendarClick(new Event("click")); expect(Renderer.TimeSelector.HideTimeDropdown).toHaveBeenCalledTimes(1); expect(document.getElementById).toHaveBeenCalledTimes(1); expect(wrapElm.getElementsByClassName).toHaveBeenCalledTimes(1); - expect(ddElm.classList.contains('hide')).toBe(true); + expect(ddElm.classList.contains("hide")).toBe(true); }); - test('Day Click', () => { - jest.spyOn(ds, 'update').mockImplementation(() => {}); - jest.spyOn(ds, 'callOnDateSelect').mockImplementation(() => {}); - jest.spyOn(Renderer.TimeSelector, 'HideTimeDropdown').mockImplementation(() => {}); + test("Day Click", () => { + jest.spyOn(ds, "update").mockImplementation(() => {}); + jest.spyOn(ds, "callOnDateSelect").mockImplementation(() => {}); + jest.spyOn(Renderer.TimeSelector, "HideTimeDropdown").mockImplementation(() => {}); ds.allowDateRangeSelection = true; ds.dayClick({ - id: '', + id: "", selectedDates: { - start: { year: 0, month: 0, day: 0 }, - end: { year: 0, month: 0, day: 0 } + start: { year: 0, month: 0, day: 0 }, + end: { year: 0, month: 0, day: 0 } } }); expect(ds.update).not.toHaveBeenCalled(); @@ -232,10 +267,10 @@ describe('Date Selector Class Tests', () => { ds.showTimeSelector = true; ds.secondDaySelect = false; ds.dayClick({ - id: '', + id: "", selectedDates: { - start: { year: 0, month: 0, day: 0 }, - end: { year: 0, month: 0, day: 0 } + start: { year: 0, month: 0, day: 0 }, + end: { year: 0, month: 0, day: 0 } } }); expect(ds.update).not.toHaveBeenCalled(); @@ -243,10 +278,10 @@ describe('Date Selector Class Tests', () => { expect(Renderer.TimeSelector.HideTimeDropdown).toHaveBeenCalledTimes(1); ds.dayClick({ - id: '', + id: "", selectedDates: { - start: { year: 0, month: 0, day: 0 }, - end: { year: 0, month: 0, day: 0 } + start: { year: 0, month: 0, day: 0 }, + end: { year: 0, month: 0, day: 0 } } }); expect(ds.update).toHaveBeenCalledTimes(1); @@ -255,10 +290,10 @@ describe('Date Selector Class Tests', () => { ds.secondDaySelect = true; ds.dayClick({ - id: '', + id: "", selectedDates: { - start: { year: 0, month: 0, day: 0 }, - end: { year: 0, month: 0, day: 0 } + start: { year: 0, month: 0, day: 0 }, + end: { year: 0, month: 0, day: 0 } } }); expect(ds.update).toHaveBeenCalledTimes(2); @@ -266,54 +301,53 @@ describe('Date Selector Class Tests', () => { expect(Renderer.TimeSelector.HideTimeDropdown).toHaveBeenCalledTimes(1); }); - test('Change Month Click', () => { - jest.spyOn(ds, 'activateListeners').mockImplementation(() => {}); - jest.spyOn(Renderer.TimeSelector, 'HideTimeDropdown').mockImplementation(() => {}); + test("Change Month Click", () => { + jest.spyOn(ds, "activateListeners").mockImplementation(() => {}); + jest.spyOn(Renderer.TimeSelector, "HideTimeDropdown").mockImplementation(() => {}); - ds.changeMonthClick(CalendarClickEvents.next, {id: '', date: {year: 0, month: 0, day: 0}}); + ds.changeMonthClick(CalendarClickEvents.next, { id: "", date: { year: 0, month: 0, day: 0 } }); expect(ds.activateListeners).toHaveBeenCalledTimes(1); expect(Renderer.TimeSelector.HideTimeDropdown).not.toHaveBeenCalled(); ds.showTimeSelector = true; - ds.changeMonthClick(CalendarClickEvents.next, {id: '', date: {year: 0, month: 0, day: 0}}); + ds.changeMonthClick(CalendarClickEvents.next, { id: "", date: { year: 0, month: 0, day: 0 } }); expect(ds.activateListeners).toHaveBeenCalledTimes(2); expect(Renderer.TimeSelector.HideTimeDropdown).toHaveBeenCalledTimes(1); - }); - test('Change Year', () => { - jest.spyOn(ds, 'activateListeners').mockImplementation(() => {}); - jest.spyOn(Renderer.TimeSelector, 'HideTimeDropdown').mockImplementation(() => {}); + test("Change Year", () => { + jest.spyOn(ds, "activateListeners").mockImplementation(() => {}); + jest.spyOn(Renderer.TimeSelector, "HideTimeDropdown").mockImplementation(() => {}); - ds.changeYear({id: '', date: {year: 0, month: 0, day: 0}}); + ds.changeYear({ id: "", date: { year: 0, month: 0, day: 0 } }); expect(ds.activateListeners).toHaveBeenCalledTimes(1); expect(Renderer.TimeSelector.HideTimeDropdown).not.toHaveBeenCalled(); ds.showTimeSelector = true; - ds.changeYear({id: '', date: {year: 0, month: 0, day: 0}}); + ds.changeYear({ id: "", date: { year: 0, month: 0, day: 0 } }); expect(ds.activateListeners).toHaveBeenCalledTimes(2); expect(Renderer.TimeSelector.HideTimeDropdown).toHaveBeenCalledTimes(1); }); - test('Add Time Click', () => { - const updateSpy = jest.spyOn(ds, 'update').mockImplementation(()=>{}); - ds.addTimeClick(new Event('click')); + test("Add Time Click", () => { + const updateSpy = jest.spyOn(ds, "update").mockImplementation(() => {}); + ds.addTimeClick(new Event("click")); expect(ds.addTime).toBe(true); expect(updateSpy).toHaveBeenCalledTimes(1); }); - test('Remove Time Click', () => { - const updateSpy = jest.spyOn(ds, 'update').mockImplementation(()=>{}); - ds.removeTimeClick(new Event('click')); + test("Remove Time Click", () => { + const updateSpy = jest.spyOn(ds, "update").mockImplementation(() => {}); + ds.removeTimeClick(new Event("click")); expect(ds.addTime).toBe(false); expect(updateSpy).toHaveBeenCalledTimes(1); }); - test('Time Change', () => { - jest.spyOn(ds, 'update').mockImplementation(() => {}); - jest.spyOn(ds, 'callOnDateSelect').mockImplementation(() => {}); + test("Time Change", () => { + jest.spyOn(ds, "update").mockImplementation(() => {}); + jest.spyOn(ds, "callOnDateSelect").mockImplementation(() => {}); - ds.timeChange({id: '', selectedTime: {start: {hour: 0, minute: 0, seconds:0}, end: {hour:0 , minute: 0, seconds: 0}}}); + ds.timeChange({ id: "", selectedTime: { start: { hour: 0, minute: 0, seconds: 0 }, end: { hour: 0, minute: 0, seconds: 0 } } }); expect(ds.update).toHaveBeenCalledTimes(1); expect(ds.callOnDateSelect).not.toHaveBeenCalled(); expect(ds.selectedDate.start.hour).toBe(0); @@ -323,7 +357,7 @@ describe('Date Selector Class Tests', () => { ds.showTimeSelector = true; ds.showDateSelector = false; - ds.timeChange({id: '', selectedTime: {start: {hour: 1, minute: 2, seconds:3}, end: {hour:0 , minute:1, seconds: 6}}}); + ds.timeChange({ id: "", selectedTime: { start: { hour: 1, minute: 2, seconds: 3 }, end: { hour: 0, minute: 1, seconds: 6 } } }); expect(ds.update).toHaveBeenCalledTimes(2); expect(ds.callOnDateSelect).toHaveBeenCalledTimes(1); expect(ds.selectedDate.start.hour).toBe(1); diff --git a/src/classes/date-selector/index.ts b/src/classes/date-selector/index.ts index 8f41d85..45a7f45 100644 --- a/src/classes/date-selector/index.ts +++ b/src/classes/date-selector/index.ts @@ -296,9 +296,9 @@ export class DateSelector { this.showCalendarYear, this.timeDelimiter ); - returnHtml = `
${calendar}${timeSelectors}
`; + returnHtml = `
${calendar}${timeSelectors}
`; if (includeWrapper) { returnHtml = `${wrapper}${returnHtml}`; } @@ -375,7 +375,7 @@ export class DateSelector { } if (html) { if (activateDomListener) { - document.addEventListener("click", this.hideCalendar.bind(this, html)); + document.addEventListener("click", this.hideCalendar); } if (activateCalendarListeners && activateTimeListeners) { const di = html.querySelector(".fsc-display-input"); @@ -415,6 +415,10 @@ export class DateSelector { } } + deactivateListeners() { + document.removeEventListener("click", this.hideCalendar); + } + /** * Will call the onDateSelect function set by the options, if set. */ @@ -463,14 +467,17 @@ export class DateSelector { * Hides the calendar portion of the input * @param {HTMLElement} html The HTMLElement for the calendar */ - hideCalendar(html: HTMLElement) { + hideCalendar = () => { this.secondDaySelect = false; - const cal = html.querySelector(".fsc-date-selector-calendar-wrapper"); - if (cal && !!(cal.offsetWidth || cal.offsetHeight || cal.getClientRects().length)) { - cal.style.display = "none"; - this.callOnDateSelect(); + const html = document.getElementById(this.id); + if (html) { + const cal = html.querySelector(".fsc-date-selector-calendar-wrapper"); + if (cal) { + cal.style.display = "none"; + this.callOnDateSelect(); + } } - } + }; /** * If the calendar container div is clicked diff --git a/src/classes/notes/note-sheet.ts b/src/classes/notes/note-sheet.ts index b8e9dfd..b278c50 100644 --- a/src/classes/notes/note-sheet.ts +++ b/src/classes/notes/note-sheet.ts @@ -222,6 +222,7 @@ export class NoteSheet extends JournalSheet { this.uiElementStates["fsc-page-list"] = false; this.uiElementStates.selectedPageIndex = 0; this.cleanUpProsemirror(); + DateSelectorManager.DeactivateSelector(this.dateSelectorId); return super.close({ submit: false }); } } diff --git a/src/classes/renderer/calendar-full.ts b/src/classes/renderer/calendar-full.ts index ba8d36f..d4ca32d 100644 --- a/src/classes/renderer/calendar-full.ts +++ b/src/classes/renderer/calendar-full.ts @@ -439,7 +439,7 @@ export default class CalendarFull { }, event: Event ) { - //event.stopPropagation(); + event.stopPropagation(); event.preventDefault(); const calendarElement = document.getElementById(calendarId); if (calendarElement) { From 9a205c191821549400a124d923c733ecd5eafad9 Mon Sep 17 00:00:00 2001 From: Dean Vigoren Date: Mon, 25 Sep 2023 21:16:41 -0600 Subject: [PATCH 2/5] Added a function that better updates the chat logs message timestamps. This new way is more compatible with other modules. Removed the no longer needed UI class for foundry interfacing --- src/classes/chat/chat-timestamp.ts | 33 +++++++++++++++++++ src/classes/foundry-interfacing/ui.test.ts | 15 --------- src/classes/foundry-interfacing/ui.ts | 13 -------- src/classes/s-c-controller.ts | 5 +-- .../sockets/render-chat-log-socket.test.ts | 10 +++--- src/classes/sockets/render-chat-log-socket.ts | 4 +-- src/classes/systems/pf2e.test.ts | 13 -------- src/classes/systems/pf2e.ts | 12 +------ src/index.ts | 3 +- 9 files changed, 46 insertions(+), 62 deletions(-) delete mode 100644 src/classes/foundry-interfacing/ui.test.ts delete mode 100644 src/classes/foundry-interfacing/ui.ts diff --git a/src/classes/chat/chat-timestamp.ts b/src/classes/chat/chat-timestamp.ts index 1e79a73..1bdb314 100644 --- a/src/classes/chat/chat-timestamp.ts +++ b/src/classes/chat/chat-timestamp.ts @@ -56,4 +56,37 @@ export class ChatTimestamp { } } } + + public static updateChatMessageTimestamps() { + const chat = document.getElementById("chat"); + if (chat) { + chat.querySelectorAll("#chat-log .message").forEach((li) => { + const message = (game).messages?.get((li).dataset.messageId || ""); + if (message) { + const formattedDateTime = this.getFormattedChatTimestamp(message); + const foundryTime = li.querySelector(".message-header .message-metadata .message-timestamp"); + const stamp = li.querySelector(".sc-timestamp"); + if (formattedDateTime && foundryTime) { + if (SC.globalConfiguration.inGameChatTimestamp) { + (foundryTime).style.display = "none"; + if (stamp) { + stamp.innerText = formattedDateTime; + } else { + const newTime = document.createElement("time"); + newTime.classList.add("sc-timestamp"); + newTime.innerText = formattedDateTime; + foundryTime.after(newTime); + } + } else { + (foundryTime).style.display = ""; + stamp?.remove(); + } + } + if (formattedDateTime && stamp) { + stamp.innerText = formattedDateTime; + } + } + }); + } + } } diff --git a/src/classes/foundry-interfacing/ui.test.ts b/src/classes/foundry-interfacing/ui.test.ts deleted file mode 100644 index c8617cc..0000000 --- a/src/classes/foundry-interfacing/ui.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @jest-environment jsdom - */ -import "../../../__mocks__/index"; -import {jest, beforeEach, describe, expect, test} from '@jest/globals'; -import Ui from "./ui"; - -describe('UI Class Tests', () => { - - test('Render Chat Log', () => { - Ui.renderChatLog(); - //@ts-ignore - expect(ui.chat.render).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/classes/foundry-interfacing/ui.ts b/src/classes/foundry-interfacing/ui.ts deleted file mode 100644 index a2cb567..0000000 --- a/src/classes/foundry-interfacing/ui.ts +++ /dev/null @@ -1,13 +0,0 @@ -export default class Ui { - public static renderChatLog() { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - ui.chat._lastId = null; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - ui.chat._state = 0; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - ui.chat.render(true); - } -} diff --git a/src/classes/s-c-controller.ts b/src/classes/s-c-controller.ts index a0710fb..beeb53a 100644 --- a/src/classes/s-c-controller.ts +++ b/src/classes/s-c-controller.ts @@ -22,7 +22,7 @@ import MultiSelect from "./renderer/multi-select"; import { GetThemeName } from "./utilities/visual"; import { FoundryVTTGameData } from "./foundry-interfacing/game-data"; import { Hook } from "./api/hook"; -import Ui from "./foundry-interfacing/ui"; +import { ChatTimestamp } from "./chat/chat-timestamp"; /** * The global Simple Calendar Controller class @@ -281,7 +281,8 @@ export default class SCController { .then( ((renderChatLog: boolean) => { if (renderChatLog) { - Ui.renderChatLog(); + ChatTimestamp.updateChatMessageTimestamps(); + //Ui.renderChatLog(); GameSockets.emit({ type: SocketTypes.renderChatLog, data: renderChatLog }).catch(Logger.error); } return GameSockets.emit({ type: SocketTypes.mainAppUpdate, data: {} }); diff --git a/src/classes/sockets/render-chat-log-socket.test.ts b/src/classes/sockets/render-chat-log-socket.test.ts index 3a94f58..67be92b 100644 --- a/src/classes/sockets/render-chat-log-socket.test.ts +++ b/src/classes/sockets/render-chat-log-socket.test.ts @@ -6,7 +6,7 @@ import { jest, beforeEach, describe, expect, test } from "@jest/globals"; import Calendar from "../calendar"; import RenderChatLogSocket from "./render-chat-log-socket"; import { SocketTypes } from "../../constants"; -import Ui from "../foundry-interfacing/ui"; +import { ChatTimestamp } from "../chat/chat-timestamp"; describe("Render Chat Log Socket Tests", () => { let tCal: Calendar; @@ -18,17 +18,17 @@ describe("Render Chat Log Socket Tests", () => { }); test("Process", async () => { - jest.spyOn(Ui, "renderChatLog").mockImplementation(() => {}); + jest.spyOn(ChatTimestamp, "updateChatMessageTimestamps").mockImplementation(() => {}); let r = await s.process({ type: SocketTypes.dateTimeChange, data: false }); expect(r).toBe(false); - expect(Ui.renderChatLog).not.toHaveBeenCalled(); + expect(ChatTimestamp.updateChatMessageTimestamps).not.toHaveBeenCalled(); r = await s.process({ type: SocketTypes.renderChatLog, data: false }); expect(r).toBe(true); - expect(Ui.renderChatLog).not.toHaveBeenCalled(); + expect(ChatTimestamp.updateChatMessageTimestamps).not.toHaveBeenCalled(); r = await s.process({ type: SocketTypes.renderChatLog, data: true }); expect(r).toBe(true); - expect(Ui.renderChatLog).toHaveBeenCalledTimes(1); + expect(ChatTimestamp.updateChatMessageTimestamps).toHaveBeenCalledTimes(1); }); }); diff --git a/src/classes/sockets/render-chat-log-socket.ts b/src/classes/sockets/render-chat-log-socket.ts index 3cf3d57..6c525a6 100644 --- a/src/classes/sockets/render-chat-log-socket.ts +++ b/src/classes/sockets/render-chat-log-socket.ts @@ -1,6 +1,6 @@ import SocketBase from "./socket-base"; import { SocketTypes } from "../../constants"; -import Ui from "../foundry-interfacing/ui"; +import { ChatTimestamp } from "../chat/chat-timestamp"; export default class RenderChatLogSocket extends SocketBase { constructor() { @@ -10,7 +10,7 @@ export default class RenderChatLogSocket extends SocketBase { async process(data: SimpleCalendar.SimpleCalendarSocket.Data): Promise { if (data.type === SocketTypes.renderChatLog) { if (data.data) { - Ui.renderChatLog(); + ChatTimestamp.updateChatMessageTimestamps(); } return true; } diff --git a/src/classes/systems/pf2e.test.ts b/src/classes/systems/pf2e.test.ts index a065d31..5ba4450 100644 --- a/src/classes/systems/pf2e.test.ts +++ b/src/classes/systems/pf2e.test.ts @@ -6,10 +6,6 @@ import { jest, beforeEach, describe, expect, test } from "@jest/globals"; import PF2E from "./pf2e"; import { LeapYearRules } from "../../constants"; import Calendar from "../calendar"; -import Ui from "../foundry-interfacing/ui"; -import { SC, updateCalManager, updateSC } from "../index"; -import CalendarManager from "../calendar/calendar-manager"; -import SCController from "../s-c-controller"; describe("Systems/PF2E Class Tests", () => { test("Update PF2E Variables", () => { @@ -145,13 +141,4 @@ describe("Systems/PF2E Class Tests", () => { PF2E.updatePF2EVariables(true); expect(PF2E.weekdayAdjust()).toBe(6); }); - - test("Update Chat Message Timestamps", () => { - updateCalManager(new CalendarManager()); - updateSC(new SCController()); - SC.globalConfiguration.inGameChatTimestamp = true; - jest.spyOn(Ui, "renderChatLog").mockImplementation(() => {}); - PF2E.updateChatMessageTimestamps(); - expect(Ui.renderChatLog).toHaveBeenCalledTimes(1); - }); }); diff --git a/src/classes/systems/pf2e.ts b/src/classes/systems/pf2e.ts index 5ac8c67..d698d04 100644 --- a/src/classes/systems/pf2e.ts +++ b/src/classes/systems/pf2e.ts @@ -4,7 +4,7 @@ import Calendar from "../calendar"; import { compareSemanticVersions } from "../utilities/string"; import { FoundryVTTGameData } from "../foundry-interfacing/game-data"; import { SC } from "../index"; -import Ui from "../foundry-interfacing/ui"; +import { Chat } from "../chat"; /** * System specific functionality for Pathfinder 2E @@ -146,14 +146,4 @@ export default class PF2E { } return adjust; } - - /** - * For every chat message update the timestamp if the Use Game Time For Chat Message Timestamps setting is enabled - * This is because chat messages are created before the PF2E world clock is initialized so we need to update them after the fact - */ - public static updateChatMessageTimestamps() { - if (SC.globalConfiguration.inGameChatTimestamp) { - Ui.renderChatLog(); - } - } } diff --git a/src/index.ts b/src/index.ts index 52a40bd..0fad6db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ import { CheckRemScaling } from "./classes/utilities/visual"; import { Hook } from "./classes/api/hook"; import KeyBindings from "./classes/key-bindings"; import { Chat } from "./classes/chat"; +import { ChatTimestamp } from "./classes/chat/chat-timestamp"; updateCalManager(new CalendarManager()); updateSC(new SCController()); @@ -63,7 +64,7 @@ Hooks.on("init", async () => { Hooks.on("ready", async () => { if (PF2E.isPF2E) { PF2E.updatePF2EVariables(true); - PF2E.updateChatMessageTimestamps(); + ChatTimestamp.updateChatMessageTimestamps(); } MigrationApplication.initialize(); //Check to see if we need to run a migration, if we do show the migration dialog otherwise show the main app From 6a4f9fde75cb9ab9fb46de9dc8faa68f2636a6bf Mon Sep 17 00:00:00 2001 From: Dean Vigoren Date: Mon, 25 Sep 2023 21:40:15 -0600 Subject: [PATCH 3/5] Updated unit tests. --- CHANGELOG.md | 5 ++- src/classes/chat/chat-timestamp.test.ts | 59 +++++++++++++++++++++++++ src/classes/chat/chat-timestamp.ts | 3 +- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6392345..bed1893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,14 @@ # Change Log -## 2.4.1 - Bug Fixes +## 2.4.2 - Bug Fixes ![](https://img.shields.io/badge/release%20date-September%2025%2C%202023-blue) -![GitHub release](https://img.shields.io/github/downloads-pre/vigoren/foundryvtt-simple-calendar/v2.4.1/module.zip) +![GitHub release](https://img.shields.io/github/downloads-pre/vigoren/foundryvtt-simple-calendar/v2.4.2/module.zip) ### Bug Fixes - Fixed an issue where selecting a date range across multiple months would close the select dialog before the second date could be chosen. ([#547](https://github.com/vigoren/foundryvtt-simple-calendar/issues/547)) +- Improved how chat message timestamps are updated for better compatibility with other modules. ([#542](https://github.com/vigoren/foundryvtt-simple-calendar/issues/542))
diff --git a/src/classes/chat/chat-timestamp.test.ts b/src/classes/chat/chat-timestamp.test.ts index 64dfd43..dc32769 100644 --- a/src/classes/chat/chat-timestamp.test.ts +++ b/src/classes/chat/chat-timestamp.test.ts @@ -100,4 +100,63 @@ describe("Chat Timestamp Tests", () => { expect(cm.getFlag).toHaveBeenCalledTimes(1); expect(ts.style.display).toBe("none"); }); + + test("Update Chat Message Timestamps", () => { + const chat = document.createElement("div"); + chat.id = "chat"; + document.body.append(chat); + + const li = document.createElement("li"); + li.dataset.messageId = "a"; + chat.append(li); + + const fTime = document.createElement("time"); + const scTime = document.createElement("span"); + + //@ts-ignore + const cqsa = jest.spyOn(chat, "querySelectorAll").mockReturnValue([li]); + const liqsa = jest.spyOn(li, "querySelector").mockReturnValue(fTime); + //@ts-ignore + game.messages = { + get: jest.fn((id: string) => { + return { id: id }; + }) + }; + jest.spyOn(ChatTimestamp, "getFormattedChatTimestamp").mockReturnValue("test"); + + // Show Foundries timestamp + ChatTimestamp.updateChatMessageTimestamps(); + expect(cqsa).toHaveBeenCalledTimes(1); + expect(liqsa).toHaveBeenCalledTimes(2); + expect(fTime.style.display).toBe(""); + + // Create SC timestamp + liqsa.mockReturnValueOnce(fTime).mockReturnValueOnce(null); + SC.globalConfiguration.inGameChatTimestamp = true; + ChatTimestamp.updateChatMessageTimestamps(); + expect(cqsa).toHaveBeenCalledTimes(2); + expect(liqsa).toHaveBeenCalledTimes(4); + expect(fTime.style.display).toBe("none"); + + // Update SC timestamp + liqsa.mockReturnValueOnce(fTime).mockReturnValueOnce(scTime); + ChatTimestamp.updateChatMessageTimestamps(); + expect(cqsa).toHaveBeenCalledTimes(3); + expect(liqsa).toHaveBeenCalledTimes(6); + expect(fTime.style.display).toBe("none"); + expect(scTime.innerText).toBe("test"); + + // FTime doesn't exist + liqsa.mockReturnValueOnce(null).mockReturnValueOnce(scTime); + ChatTimestamp.updateChatMessageTimestamps(); + expect(cqsa).toHaveBeenCalledTimes(4); + expect(liqsa).toHaveBeenCalledTimes(8); + expect(scTime.innerText).toBe("test"); + + //No dataset id + li.dataset.messageId = ""; + ChatTimestamp.updateChatMessageTimestamps(); + expect(cqsa).toHaveBeenCalledTimes(5); + expect(liqsa).toHaveBeenCalledTimes(10); + }); }); diff --git a/src/classes/chat/chat-timestamp.ts b/src/classes/chat/chat-timestamp.ts index 1bdb314..4c4dada 100644 --- a/src/classes/chat/chat-timestamp.ts +++ b/src/classes/chat/chat-timestamp.ts @@ -81,8 +81,7 @@ export class ChatTimestamp { (foundryTime).style.display = ""; stamp?.remove(); } - } - if (formattedDateTime && stamp) { + } else if (formattedDateTime && stamp) { stamp.innerText = formattedDateTime; } } From 47bb22a8aa146fc32deaf1aa875a0ecc726ff3d0 Mon Sep 17 00:00:00 2001 From: Dean Vigoren Date: Mon, 25 Sep 2023 21:53:21 -0600 Subject: [PATCH 4/5] Updated how chat timestamps are displayed. --- CHANGELOG.md | 5 +++-- src/styles/scaffolding/chat.scss | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bed1893..c9d75a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,15 @@ # Change Log -## 2.4.2 - Bug Fixes +## 2.4.3 - Bug Fixes ![](https://img.shields.io/badge/release%20date-September%2025%2C%202023-blue) -![GitHub release](https://img.shields.io/github/downloads-pre/vigoren/foundryvtt-simple-calendar/v2.4.2/module.zip) +![GitHub release](https://img.shields.io/github/downloads-pre/vigoren/foundryvtt-simple-calendar/v2.4.3/module.zip) ### Bug Fixes - Fixed an issue where selecting a date range across multiple months would close the select dialog before the second date could be chosen. ([#547](https://github.com/vigoren/foundryvtt-simple-calendar/issues/547)) - Improved how chat message timestamps are updated for better compatibility with other modules. ([#542](https://github.com/vigoren/foundryvtt-simple-calendar/issues/542)) +- Improved how chat message timestamps are displayed for better compatibility with other modules.([#545](https://github.com/vigoren/foundryvtt-simple-calendar/issues/545))
diff --git a/src/styles/scaffolding/chat.scss b/src/styles/scaffolding/chat.scss index a690dab..7708f04 100644 --- a/src/styles/scaffolding/chat.scss +++ b/src/styles/scaffolding/chat.scss @@ -1,5 +1,5 @@ .chat-sidebar{ .sc-timestamp{ - display: block; + display: inline-block; } } From f91b19ba0408cf015401dc817024a2c8808685a4 Mon Sep 17 00:00:00 2001 From: Dean Vigoren Date: Mon, 25 Sep 2023 22:05:31 -0600 Subject: [PATCH 5/5] Updated version number and changelog --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- src/module.json | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d75a8..a31fe75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ - Improved how chat message timestamps are updated for better compatibility with other modules. ([#542](https://github.com/vigoren/foundryvtt-simple-calendar/issues/542)) - Improved how chat message timestamps are displayed for better compatibility with other modules.([#545](https://github.com/vigoren/foundryvtt-simple-calendar/issues/545)) +### Translation Updates + +Thank you to the follow people for making updates to Simple Calendars translations: + +- [Jakub](https://weblate.foundryvtt-hub.com/user/Lioheart/) (Polish) +- [vincent](https://weblate.foundryvtt-hub.com/user/rectulo/) (French) +- [Martin Matoška](https://weblate.foundryvtt-hub.com/user/Mortan/) (Czech) +- [Sven Hesse](https://weblate.foundryvtt-hub.com/user/DrMcCoy/) (German) +
## 2.4.0 - Leap Year Starting Year and Bug Fixes diff --git a/package.json b/package.json index 3045091..8ce8cd7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "foundryvtt-simple-calendar", "description": "A simple calendar module for keeping track of game days and events.", - "version": "2.4.0", + "version": "2.4.3", "author": "Dean Vigoren (vigorator)", "keywords": [ "foundryvtt", diff --git a/src/module.json b/src/module.json index 022c2b1..17531a0 100644 --- a/src/module.json +++ b/src/module.json @@ -3,7 +3,7 @@ "name": "foundryvtt-simple-calendar", "title": "Simple Calendar", "description": "A simple calendar module for keeping track of game days and events.", - "version": "2.4.0", + "version": "2.4.3", "authors": [ { "name": "Dean Vigoren (Vigorator)",