From e50347d09df6abd93ee5c7acc4c965d66d78b549 Mon Sep 17 00:00:00 2001 From: Brad Coudriet Date: Wed, 15 Apr 2026 20:15:10 -0400 Subject: [PATCH] added orderByTimeLeft field to display events in order --- MMM-CountEvents.js | 262 ++++++++++++++++++++++++++++----------------- README.md | 85 +++++++++++---- 2 files changed, 226 insertions(+), 121 deletions(-) diff --git a/MMM-CountEvents.js b/MMM-CountEvents.js index e903558..2ace9ac 100644 --- a/MMM-CountEvents.js +++ b/MMM-CountEvents.js @@ -1,5 +1,4 @@ Module.register("MMM-CountEvents", { - defaults: { refresh: 1000 * 60, title: "nonamed", @@ -19,125 +18,188 @@ Module.register("MMM-CountEvents", { useQuarter: false, onPassed: null, onUpdated: null, - - events:[], + orderByTimeLeft: null, // 'asc', 'desc', or null for default list order + events: [], }, - getStyles: function() { - return ["MMM-CountEvents.css"] + getStyles: function () { + return ["MMM-CountEvents.css"]; }, - start: function() { - this.config.identifier = this.config?.identifier ?? 'CE_' + this.identifier - this.config.locale = this.config?.locale ?? config?.locale ?? "en" + start: function () { + this.config.identifier = this.config?.identifier ?? "CE_" + this.identifier; + this.config.locale = this.config?.locale ?? config?.locale ?? "en"; }, - getDom: function() { - const wrapper = document.createElement("ul") - wrapper.classList.add('CE') - wrapper.id = this.config.identifier - this.config.events.forEach(ev => { - const event = this.regularize(ev) - const mmt = this.mmTime(event) - if (!mmt) return + getTargetTime: function (event) { + const t = new Date(event.targetTime); + const n = new Date(Date.now()); + const tMonth = t.getMonth(); + const tDate = t.getDate(); + const tHour = t.getHours(); + const tMinute = t.getMinutes(); + const tSecond = t.getSeconds(); + const tMilisecond = t.getMilliseconds(); + const nYear = n.getFullYear(); + const nMonth = n.getMonth(); + const nDate = n.getDate(); + const nHour = n.getHours(); - const eventWrapper = document.createElement("li") - if (event.className) eventWrapper.classList.add(event.className) - eventWrapper.innerHTML = event.output - eventWrapper.querySelector(".title").innerHTML = event.title - eventWrapper.querySelector(".output").appendChild(mmt) - wrapper.appendChild(eventWrapper) - }) - return wrapper + let nextT = new Date(t); + switch (event.repeat) { + case "yearly": + nextT = new Date( + nYear, + tMonth, + tDate, + tHour, + tMinute, + tSecond, + tMilisecond, + ); + if (nextT.valueOf() < n.valueOf()) nextT.setFullYear(nYear + 1); + break; + case "monthly": + nextT = new Date( + nYear, + nMonth, + tDate, + tHour, + tMinute, + tSecond, + tMilisecond, + ); + if (nextT.valueOf() < n.valueOf()) nextT.setMonth(nMonth + 1); + if (nextT.getDate() !== tDate) nextT.setDate(0); + break; + case "weekly": + nextT = new Date( + nYear, + nMonth, + nDate, + tHour, + tMinute, + tSecond, + tMilisecond, + ); + const dayDiff = t.getDay() - nextT.getDay(); + if (dayDiff === 0 && n.valueOf() > nextT.valueOf()) + nextT.setDate(nDate + 7); + if (dayDiff !== 0) nextT.setDate(nDate + ((dayDiff + 7) % 7)); + break; + case "daily": + nextT = new Date( + nYear, + nMonth, + nDate, + tHour, + tMinute, + tSecond, + tMilisecond, + ); + if (nextT.valueOf() < n.valueOf()) nextT.setDate(nDate + 1); + break; + case "hourly": + nextT = new Date( + nYear, + nMonth, + nDate, + nHour, + tMinute, + tSecond, + tMilisecond, + ); + if (nextT.valueOf() < n.valueOf()) nextT.setHours(nHour + 1); + break; + default: + break; + } + return nextT; }, - regularize: function(event) { - const { events, identifier, ...rest } = this.config - return { ...rest, ...event } - }, + getDom: function () { + const wrapper = document.createElement("ul"); + wrapper.classList.add("CE"); + wrapper.id = this.config.identifier; - mmTime: function(event) { - const repeated = (event) => { - const t = new Date(event.targetTime) - const n = new Date(Date.now()) - const tMonth = t.getMonth() - const tDate = t.getDate() - const tHour = t.getHours() - const tMinute = t.getMinutes() - const tSecond = t.getSeconds() - const tMilisecond = t.getMilliseconds() - const nYear = n.getFullYear() - const nMonth = n.getMonth() - const nDate = n.getDate() - const nHour = n.getHours() + let events = this.config.events.slice(); - let nextT = new Date(t) - switch (event.repeat) { - case "yearly": - nextT = new Date(nYear, tMonth, tDate, tHour, tMinute, tSecond, tMilisecond) - if (nextT.valueOf() < n.valueOf()) nextT.setFullYear(nYear + 1) - break - case "monthly": - nextT = new Date(nYear, nMonth, tDate, tHour, tMinute, tSecond, tMilisecond) - if (nextT.valueOf() < n.valueOf()) nextT.setMonth(nMonth + 1) - if (nextT.getDate() !== tDate) nextT.setDate(0) - break - case "weekly": - nextT = new Date(nYear, nMonth, nDate, tHour, tMinute, tSecond, tMilisecond) - const dayDiff = t.getDay() - nextT.getDay() - if (dayDiff === 0 && n.valueOf() > nextT.valueOf()) nextT.setDate(nDate + 7) - if (dayDiff !== 0) nextT.setDate(nDate + ((dayDiff + 7) % 7)) - break - case "daily": - nextT = new Date(nYear, nMonth, nDate, tHour, tMinute, tSecond, tMilisecond) - if (nextT.valueOf() < n.valueOf()) nextT.setDate(nDate + 1) - break - case "hourly": - nextT = new Date(nYear, nMonth, nDate, nHour, tMinute, tSecond, tMilisecond) - if (nextT.valueOf() < n.valueOf()) nextT.setHours(nHour + 1) - break - default: break - } - return nextT + if (this.config.orderByTimeLeft) { + events.sort((a, b) => { + const aTime = this.getTargetTime(this.regularize(a)).valueOf(); + const bTime = this.getTargetTime(this.regularize(b)).valueOf(); + if (this.config.orderByTimeLeft === "asc") return aTime - bTime; + if (this.config.orderByTimeLeft === "desc") return bTime - aTime; + return 0; + }); } - const targetTime = new Date(repeated(event)) - const now = Date.now() - if (event.ignoreBefore && targetTime.valueOf() - +event.ignoreBefore > now) return false - if (event.ignoreAfter && targetTime.valueOf() + +event.ignoreAfter < now) return false + events.forEach((ev) => { + const event = this.regularize(ev); + const mmt = this.mmTime(event); + if (!mmt) return; + + const eventWrapper = document.createElement("li"); + if (event.className) eventWrapper.classList.add(event.className); + eventWrapper.innerHTML = event.output; + eventWrapper.querySelector(".title").innerHTML = event.title; + eventWrapper.querySelector(".output").appendChild(mmt); + wrapper.appendChild(eventWrapper); + }); + return wrapper; + }, + + regularize: function (event) { + const { events, identifier, ...rest } = this.config; + return { ...rest, ...event }; + }, - const mmt = document.createElement("mm-time") - mmt.relative = true - mmt.locale = event?.locale ?? this.config?.locale ?? config?.locale ?? "en" - mmt.time = targetTime - mmt.decouple = true - mmt.dataset.numeric = (event.numericAlways) ? 'always' : 'auto' - if (event.unit) mmt.relativeUnit = event.unit - if (event.reverse) mmt.relativeReverse = event.reverse - if (event.refresh && event.refresh >= 1000) mmt.refresh = event.refresh - if (event.useQuarter) mmt.relativeQuarter = true - if (event.numberOnly) mmt.classList.add("number-only") - if (event.numberSign) mmt.classList.add("number-sign") - mmt.classList.add((targetTime.valueOf() < now) ? "past" : ((targetTime.valueOf() > now) ? "future" : "now")) - if (event.className) mmt.classList.add(event.className) + mmTime: function (event) { + const targetTime = new Date(this.getTargetTime(event)); + const now = Date.now(); + if (event.ignoreBefore && targetTime.valueOf() - +event.ignoreBefore > now) + return false; + if (event.ignoreAfter && targetTime.valueOf() + +event.ignoreAfter < now) + return false; + + const mmt = document.createElement("mm-time"); + mmt.relative = true; + mmt.locale = event?.locale ?? this.config?.locale ?? config?.locale ?? "en"; + mmt.time = targetTime; + mmt.decouple = true; + mmt.dataset.numeric = event.numericAlways ? "always" : "auto"; + if (event.unit) mmt.relativeUnit = event.unit; + if (event.reverse) mmt.relativeReverse = event.reverse; + if (event.refresh && event.refresh >= 1000) mmt.refresh = event.refresh; + if (event.useQuarter) mmt.relativeQuarter = true; + if (event.numberOnly) mmt.classList.add("number-only"); + if (event.numberSign) mmt.classList.add("number-sign"); + mmt.classList.add( + targetTime.valueOf() < now + ? "past" + : targetTime.valueOf() > now + ? "future" + : "now", + ); + if (event.className) mmt.classList.add(event.className); mmt.onPassed = (ev) => { - let ret = null + let ret = null; if (typeof event?.onPassed === "function") { - ret = event.onPassed(event, mmt) + ret = event.onPassed(event, mmt); } - if (event.repeat) mmt.time = new Date(repeated(event)) - return ret - } + if (event.repeat) mmt.time = new Date(this.getTargetTime(event)); + return ret; + }; mmt.onUpdated = (ev) => { - let ret = null + let ret = null; if (typeof event?.onUpdated === "function") { - ret = event.onUpdated(event, mmt) + ret = event.onUpdated(event, mmt); } - return ret - } + return ret; + }; if (typeof event?.manipulate === "function") { - return event.manipulate(mmt) + return event.manipulate(mmt); } - return mmt + return mmt; }, -}) \ No newline at end of file +}); diff --git a/README.md b/README.md index fea05ca..ce9d14c 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,24 @@ # MMM-CountEvents + Countdown or countup for events > This module is revamped from the scratch. If you are using the previous version, you may need reinstall again. ## Screenshots + ![screenshot](./screenshot.png) ![screenshot](screenshots/sc1.png) ## Features + - Count up / down to specific date/time - Various formatting & custom template - Auto repeat : yearly, monthly, daily, hourly. - Callback functions for updating or passing the event. - ## Installation + ```sh cd ~/MagicMirror/modules git clone https://github.com/MMRIZE/MMM-CountEvents @@ -24,6 +27,7 @@ npm install ``` This module needs `MMM-CustomElementTime` module also. It would be installed by execution of `postinstall.sh`. Usually This script will be run automatically by `npm install` but, when that is not executed properly, do it by manual. + ```sh # In some environments, you may need to allow permission chmod 755 ./postinstall.sh @@ -34,19 +38,21 @@ sh ./postinstall.sh ``` But when you have still a problem, you can install that module also by manual. + ```sh cd ~/MagicMirror/modules git clone https://github.com/MMRIZE/MMM-CustomElementTime ``` - ## Configuration + **IMPORTANT** You should include `MMM-CustomElementTime` also in the `config.js` + ```js { - module: "MMM-CustomElementTime" + module: "MMM-CustomElementTime" }, { module: "MMM-CountEvents", @@ -63,6 +69,7 @@ You should include `MMM-CustomElementTime` also in the `config.js` ``` ### Simplest Example + ```js { module: "MMM-CountEvents", @@ -77,7 +84,9 @@ You should include `MMM-CustomElementTime` also in the `config.js` } }, ``` + ### Detailed (and default) + ```js { module: "MMM-CountEvents", @@ -99,7 +108,7 @@ You should include `MMM-CustomElementTime` also in the `config.js` useQuarter: false, onPassed: null, onUpdated: null, - + orderByTimeLeft: null, events: [ { title: "To Christmas", @@ -113,13 +122,14 @@ You should include `MMM-CustomElementTime` also in the `config.js` ``` ### Config values for common default properties + These properties in `config:[]` will be applied to each event by default. You can reassign specific property in each event again. |Property | default value | Description | |:---|:---:|:---| |`locale`|MM's default `locale` or system locale | `BCP-47` format locale identifier. It needs for displaying format by user's locale.
e.g. "de", "en-CA" | |`refresh`| 60_000 | (ms) self-refreshing interval of the event. `0` means no refreshing. |`unit`| "auto" | **Available values**: `"auto"`, `"year"`, `"month"`, `"day"`, `"hour"`, `"minute"`, `"second"` and `"quarter"`
`"years"`, `"months"`, `"days"`, `"hours"`, `"minutes"`, `"seconds"` would be available also.| -|`repeat`| false | **Available values**: `"yearly"`, `"monthly"`, `"daily"`, `"hourly"` or `false`
When the event is a kind of recurred events.
`"minutely"` and `"secondly"` are not supported. It sounds weird and out-of-sense. | +|`repeat`| false | **Available values**: `"yearly"`, `"monthly"`, `"daily"`, `"hourly"` or `false`
When the event is a kind of recurred events.
`"minutely"` and `"secondly"` are not supported. It sounds weird and out-of-sense. | |`ignoreBefore`| false | **Available values**: `false` or `(miliseconds)`
If you set `1000 * 60 * 60 * 24 * 7`, this events will not be displayed before 7days from `targetTime`. | |`ignoreAfter` | false | **Available values**: `false` or `(miliseconds)`
If you set `1000 * 60 * 60`, this events will not be displayed after 1 hour from `targetTime`.| |`className`| "default" | You can assign specific CSS class name to the event. | @@ -131,29 +141,49 @@ These properties in `config:[]` will be applied to each event by default. You ca |`useQuarter` | false | Add `quarter` unit into the `unit: "auto"` options. | |`onPassed` | callback function `(event, element)`| When the `targetTime` is passed, this callback function would be called. Usually this will be used for an alarm function. See the [Examples]
- The check for passing moment is depending on the `refresh` interval.
- When if the event is repeated, this function would be called again at a time.| |`onUpdated` | callback function `(event, element)` | When the event is refreshed by `refresh` option, this callback function would be called. Usually this will be used for modifying the result on real-time. See the [Examples]| +|`orderByTimeLeft` | null | **Available values**: `null`, `"asc"`, `"desc"`
Orders the events by time left to the target time. `"asc"` for closest first, `"desc"` for farthest first, `null` for default list order.| |`events` | [] | Array of event objects to display. | ### Event object -|Property | default value | Description | -|:---|:---:|:---| -|`title`| "nonamed" | The title of the event. This value will be injected into the first `class="title"` element in `output` template of the event. | -|`targetTime` | "2025-01-01" | The target time of the event.
**Available Types**
- *Unix Timestamp*
- *Date-like String*: See the below [[Time format]]
- *Javascript Date Object* | + +| Property | default value | Description | +| :----------- | :-----------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `title` | "nonamed" | The title of the event. This value will be injected into the first `class="title"` element in `output` template of the event. | +| `targetTime` | "2025-01-01" | The target time of the event.
**Available Types**
- _Unix Timestamp_
- _Date-like String_: See the below [[Time format]]
- _Javascript Date Object_ | + > All common default properties could be used and reassigned for the specific evnet object. ## CSS Styling + The displaying result of this module woule have the HTML like this. (with default `output` template) + ```html -
+
    /* event begin */ -
  • /* className of the event */ - /* output template begin */ +
  • + /* className of the event */ /* output template begin */
    -
    Daily Closing (Repeating)
    /* title injection */ -
    /* result injection */ - /* mm-time custom element to display the targetTime */ +
    Daily Closing (Repeating)
    + /* title injection */ +
    + /* result injection */ + + /* mm-time custom element to display the targetTime */ in 23 hours @@ -171,9 +201,11 @@ The displaying result of this module woule have the HTML like this. (with defaul You can handle CSS to style for your purpose. ## Examples + ### Various configs ![screenshot](./screenshot.png) + ```js { module: "MMM-CountEvents", @@ -197,7 +229,7 @@ You can handle CSS to style for your purpose. targetTime: "2025-01-01", locale: "ja-JP", numberOnly: true, - numberSign: true, + numberSign: true, unit: "days", output: `
    D ()
    `, }, @@ -215,8 +247,11 @@ You can handle CSS to style for your purpose. } }, ``` + ### `onPassed` example for notification alert + ![screenshot](./screenshots/sc2.png) + ```js { module: "MMM-CountEvents", @@ -231,7 +266,7 @@ You can handle CSS to style for your purpose. onPassed: (event, element) => { const self = MM.getModules().withClass("MMM-CountEvents")[0] MM.sendNotification( - "SHOW_ALERT", + "SHOW_ALERT", { type: "alert", title: "Daily Closing", message: "It's time to close the shop.", timer: 60_000 }, self ) @@ -241,18 +276,21 @@ You can handle CSS to style for your purpose. } }, ``` + ### `onUpdated` example for displaying **`Years, Months, Days, Hours, Minutes, Seconds`** + This module doesn't provide "OO years, OO months, OO days, OO hours, OO minutes, OO seconds" format, however, you can hook the result with `onUpdated` callback function. ![screenshot](screenshots/sc1.png) + ```js /* event object definition*/ title: "To Christmas", targetTime: "2024-12-25", repeat: "yearly", onUpdated: (event, element) => { - const pluralMap = { - // In 'english-something' locales, 'one' and 'other' are only used. + const pluralMap = { + // In 'english-something' locales, 'one' and 'other' are only used. // However, other locales may have more. ('zero', 'two', 'few', 'many' in 'arabic' locale. 'Chinese' locale has only 'other') // You need to modify this map according to your locale. "year": { one: "year", other: "years" }, @@ -297,10 +335,11 @@ onUpdated: (event, element) => { ``` ### Time format -For `targetTime`, [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) and [RFC 2822](https://tools.ietf.org/html/rfc2822#section-3.3) available. +For `targetTime`, [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) and [RFC 2822](https://tools.ietf.org/html/rfc2822#section-3.3) available. **[RFC 2822]** + ``` 6 Mar 17 21:22 UT 6 Mar 17 21:22:23 UT @@ -311,6 +350,7 @@ Mon, 06 Mar 2017 21:22:23 +0000 ``` **[ISO 8601]** + ``` 2013-02-08 # A calendar date part 2013-W06-5 # A week date part @@ -346,11 +386,14 @@ Mon, 06 Mar 2017 21:22:23 +0000 ``` ## History + ### `2.0.0` (2024-08-07) + - Revamped ## Author -- Author: Seongnoh Sean Yi + +- Author: Seongnoh Sean Yi [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Y8Y56IFLK)