senangwebs-epoch 1.1.3 → 1.1.4

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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  SenangWebs Epoch (SWE) is a lightweight JavaScript library for creating dynamic countdown timers and time displays. Whether you need to count down to a specific date, create a duration-based timer, or display the current time, SWE provides an elegant solution with minimal setup. The library supports flexible formatting, responsive design, and comprehensive event handling.
4
4
 
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
6
+
5
7
  ## Features
6
8
 
7
9
  - Multiple timer modes:
@@ -12,8 +14,9 @@ SenangWebs Epoch (SWE) is a lightweight JavaScript library for creating dynamic
12
14
  - Customizable time unit formatting
13
15
  - Responsive design out of the box
14
16
  - Comprehensive event system
15
- - Pause, resume, and reset functionality
16
- - No external dependencies
17
+ - Pause, resume, and reset functionality
18
+ - Completed and expired countdowns stop cleanly and emit `swe:end` once
19
+ - No external dependencies
17
20
  - Built-in default styling with CSS
18
21
  - Works on all modern browsers
19
22
 
@@ -48,7 +51,7 @@ Include SenangWebs Epoch directly in your HTML file:
48
51
  ### Countdown to a specific date
49
52
 
50
53
  ```html
51
- <div data-swe data-swe-countdown-end="2025-01-01 00:00:00">
54
+ <div data-swe data-swe-countdown-end="2027-01-01T00:00:00">
52
55
  <div data-swe-year data-swe-format="yyyy"></div>
53
56
  <div data-swe-month data-swe-format="mm"></div>
54
57
  <div data-swe-day data-swe-format="dd"></div>
@@ -119,7 +122,7 @@ The format string determines zero-padding based on its length:
119
122
  const timer = new SWE(element, {
120
123
  autostart: true,
121
124
  duration: 60, // for countdown-duration mode
122
- countdownEnd: '2025-01-01 00:00:00', // for countdown-end mode
125
+ countdownEnd: '2027-01-01T00:00:00', // ISO 8601, interpreted as local time
123
126
  onTick: () => {
124
127
  console.log('Timer ticked!');
125
128
  },
@@ -166,14 +169,18 @@ timer.element.addEventListener('swe:end', (e) => {
166
169
  });
167
170
  ```
168
171
 
169
- Available events:
172
+ Available events:
170
173
  - `swe:start`
171
174
  - `swe:tick`
172
175
  - `swe:pause`
173
176
  - `swe:resume`
174
177
  - `swe:reset`
175
178
  - `swe:stop`
176
- - `swe:end`
179
+ - `swe:end`
180
+
181
+ `swe:end` is emitted once when a countdown reaches zero. A countdown that is
182
+ already expired during initialization displays zero and does not leave a
183
+ background interval running.
177
184
 
178
185
  ## Styling
179
186
 
package/SKILLS.md ADDED
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: senangwebs-epoch
3
+ description: Dynamic countdown timers, duration timers, and current-time displays with flexible formatting and event callbacks.
4
+ version: 1.1.4
5
+ package: senangwebs-epoch
6
+ ---
7
+
8
+ # SenangWebs Epoch (SWE)
9
+
10
+ ## Quick Reference
11
+
12
+ - **Purpose**: Countdown-to-date, countdown-duration, and current-time displays
13
+ - **Entry**: `dist/swe.js`
14
+ - **Dependencies**: none
15
+ - **Scripts**: `npm run build`, `npm run dev`, `npm run test`
16
+
17
+ ## Workflow
18
+
19
+ Start in `C:\wamp64\www\sw-libraries\senangwebs-epoch`. Read `README.md`, `package.json`, and touched source files. Match existing patterns, CSS prefix `swe-`.
20
+
21
+ ## HTML Data Attributes
22
+
23
+ ### Container
24
+ | Attribute | Values |
25
+ |---|---|
26
+ | `data-swe` | flag |
27
+ | `data-swe-countdown-end` | ISO 8601 date string |
28
+ | `data-swe-countdown-duration` | non-negative duration in seconds |
29
+ | `data-swe-current` | flag for current-time mode |
30
+
31
+ ### Time unit display elements
32
+ | Attribute | Description |
33
+ |---|---|
34
+ | `data-swe-year` | container for years |
35
+ | `data-swe-month` | container for months |
36
+ | `data-swe-day` | container for days |
37
+ | `data-swe-hour` | container for hours |
38
+ | `data-swe-minute` | container for minutes |
39
+ | `data-swe-second` | container for seconds |
40
+ | `data-swe-format` | padding token; its length sets the minimum width |
41
+
42
+ ## JavaScript API
43
+
44
+ ```js
45
+ const timer = new SWE(element, {
46
+ autostart, countdownEnd, duration,
47
+ onTick, onEnd, onStart, onPause, onResume, onReset, onStop
48
+ })
49
+
50
+ timer.start()
51
+ timer.pause()
52
+ timer.resume()
53
+ timer.reset()
54
+ timer.stop()
55
+ ```
56
+
57
+ ### DOM Events
58
+ `swe:tick`, `swe:end`, `swe:start`, `swe:pause`, `swe:resume`, `swe:reset`, `swe:stop`
59
+
60
+ ## Focus Areas
61
+
62
+ - Countdown accuracy: handle timezone differences, past dates, invalid dates
63
+ - Duration mode: decrementing timer with pause/resume/reset
64
+ - Clock mode: real-time display updating every second
65
+ - Format options: zero-padded vs natural number display
66
+ - Completed timer state: display zero, do not create or retain an interval, fire `onEnd`/`swe:end` once
67
+
68
+ ## Implementation Guidance
69
+
70
+ - Preserve backward compatibility for all attributes and event names
71
+ - Handle edge cases: invalid dates should show error state, not crash
72
+ - Use ISO 8601 countdown dates such as `2027-01-01T00:00:00`
73
+ - Test across timezone changes (DST boundaries)
74
+ - Verify pause/resume doesn't drift accumulated time
75
+
76
+ ## Validation
77
+
78
+ ```bash
79
+ npm run build
80
+ npm test
81
+ ```
package/dist/swe.js CHANGED
@@ -1 +1 @@
1
- !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SWE=e():t.SWE=e()}(this,(()=>(()=>{"use strict";var t={d:(e,n)=>{for(var i in n)t.o(n,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:n[i]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},e={};function n(t){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},n(t)}function i(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=Array(e);n<e;n++)i[n]=t[n];return i}function o(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(t,r(i.key),i)}}function r(t){var e=function(t){if("object"!=n(t)||!t)return t;var e=t[Symbol.toPrimitive];if(void 0!==e){var i=e.call(t,"string");if("object"!=n(i))return i;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(t)}(t);return"symbol"==n(e)?e:e+""}t.d(e,{default:()=>s});var a=function(){return t=function t(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.element=e,this.options={autostart:void 0===n.autostart||n.autostart,duration:n.duration,countdownEnd:n.countdownEnd,onTick:n.onTick,onEnd:n.onEnd,onStart:n.onStart,onPause:n.onPause,onResume:n.onResume,onReset:n.onReset,onStop:n.onStop},this.intervalId=null,this.isPaused=!1,this.startTime=null,this.pausedTimeRemaining=null,this.endTime=null,this.initialize()},(e=[{key:"initialize",value:function(){if(!(this.element&&this.element instanceof HTMLElement))throw new Error("SWE: Invalid element provided");if(this.mode=this.determineMode(),this.setupElements(),this.options.autostart)this.start();else if("countdown-duration"===this.mode&&this.options.duration)this.updateDisplay(1e3*this.options.duration);else if("countdown-end"===this.mode&&this.options.countdownEnd){var t=new Date(this.options.countdownEnd),e=new Date,n=t.getTime()-e.getTime();this.updateDisplay(n>0?n:0)}}},{key:"determineMode",value:function(){if(this.options.countdownEnd)return"countdown-end";if(this.options.duration)return"countdown-duration";if(this.element.hasAttribute("data-swe-countdown-end"))return"countdown-end";if(this.element.hasAttribute("data-swe-countdown-duration"))return"countdown-duration";if(this.element.hasAttribute("data-swe-current"))return"current";if(this.options.countdownEnd)return"countdown-end";throw new Error("SWE: Invalid mode - missing required configuration")}},{key:"setupElements",value:function(){this.elements={year:this.element.querySelector("[data-swe-year]"),month:this.element.querySelector("[data-swe-month]"),day:this.element.querySelector("[data-swe-day]"),hour:this.element.querySelector("[data-swe-hour]"),minute:this.element.querySelector("[data-swe-minute]"),second:this.element.querySelector("[data-swe-second]")}}},{key:"start",value:function(){if(!this.intervalId){switch(this.isPaused=!1,this.startTime=Date.now(),this.mode){case"countdown-end":this.startCountdownToDate();break;case"countdown-duration":this.startCountdownDuration();break;case"current":this.startCurrentTime()}this.dispatchEvent("start")}}},{key:"startCountdownToDate",value:function(){var t=new Date(this.options.countdownEnd||this.element.getAttribute("data-swe-countdown-end"));if(isNaN(t.getTime()))throw new Error("SWE: Invalid end date format");this.endTime=t.getTime(),this.updateTimer()}},{key:"startCountdownDuration",value:function(){var t=this.options.duration||parseInt(this.element.getAttribute("data-swe-countdown-duration"),10);if(isNaN(t)||t<0)throw new Error("SWE: Invalid duration");null!==this.pausedTimeRemaining?this.endTime=Date.now()+this.pausedTimeRemaining:this.endTime=Date.now()+1e3*t,this.updateTimer()}},{key:"updateTimer",value:function(){var t=this,e=function(){var e=Date.now(),n=t.endTime-e;if(n<=0)return t.stop(),t.updateDisplay(0),void t.dispatchEvent("end");t.updateDisplay(n),t.dispatchEvent("tick")};e(),this.intervalId=setInterval(e,1e3)}},{key:"startCurrentTime",value:function(){var t=this,e=function(){var e=new Date;t.updateCurrentTimeDisplay(e),t.dispatchEvent("tick")};e(),this.intervalId=setInterval(e,1e3)}},{key:"updateDisplay",value:function(t){var e=this,n=this.calculateTimeUnits(t);Object.entries(this.elements).forEach((function(t){var o=function(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=n){var i,o,r,a,s=[],u=!0,d=!1;try{if(r=(n=n.call(t)).next,0===e){if(Object(n)!==n)return;u=!1}else for(;!(u=(i=r.call(n)).done)&&(s.push(i.value),s.length!==e);u=!0);}catch(t){d=!0,o=t}finally{try{if(!u&&null!=n.return&&(a=n.return(),Object(a)!==a))return}finally{if(d)throw o}}return s}}(t,e)||function(t,e){if(t){if("string"==typeof t)return i(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?i(t,e):void 0}}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}(t,2),r=o[0],a=o[1];a&&e.updateElement(a,n[r])}))}},{key:"updateCurrentTimeDisplay",value:function(t){this.elements.hour&&this.updateElement(this.elements.hour,t.getHours()),this.elements.minute&&this.updateElement(this.elements.minute,t.getMinutes()),this.elements.second&&this.updateElement(this.elements.second,t.getSeconds())}},{key:"updateElement",value:function(t,e){var n=t.getAttribute("data-swe-format");t.textContent=this.formatValue(e,n)}},{key:"formatValue",value:function(t,e){return e?t.toString().padStart(e.length,"0"):t.toString()}},{key:"calculateTimeUnits",value:function(t){var e=Math.floor(t/1e3),n=Math.floor(e/60),i=Math.floor(n/60),o=(Math.floor(i/24),new Date),r=new Date(o.getTime()+t),a=r.getFullYear()-o.getFullYear(),s=r.getMonth()-o.getMonth(),u=r.getDate()-o.getDate();return u<0&&(s--,u+=new Date(r.getFullYear(),r.getMonth(),0).getDate()),s<0&&(a--,s+=12),{year:a,month:s,day:u,hour:i%24,minute:n%60,second:e%60}}},{key:"dispatchEvent",value:function(t){var e="on".concat(t.charAt(0).toUpperCase()+t.slice(1));"function"==typeof this.options[e]&&this.options[e].call(this);var n=new CustomEvent("swe:".concat(t),{bubbles:!0,detail:{instance:this}});this.element.dispatchEvent(n)}},{key:"pause",value:function(){this.intervalId&&!this.isPaused&&(clearInterval(this.intervalId),this.intervalId=null,this.isPaused=!0,this.pausedTimeRemaining=this.endTime-Date.now(),this.dispatchEvent("pause"))}},{key:"resume",value:function(){if(this.isPaused){switch(this.isPaused=!1,this.startTime=Date.now(),this.mode){case"countdown-end":this.startCountdownToDate();break;case"countdown-duration":this.startCountdownDuration();break;case"current":this.startCurrentTime()}this.dispatchEvent("resume")}}},{key:"reset",value:function(){if(this.stop(),this.pausedTimeRemaining=null,this.endTime=null,"countdown-end"===this.mode&&this.options.countdownEnd){var t=new Date(this.options.countdownEnd),e=new Date,n=t.getTime()-e.getTime();this.updateDisplay(n>0?n:0)}else"countdown-duration"===this.mode&&this.options.duration&&this.updateDisplay(1e3*this.options.duration);this.dispatchEvent("reset"),this.options.autostart&&this.start()}},{key:"stop",value:function(){var t=null!==this.intervalId;this.intervalId&&(clearInterval(this.intervalId),this.intervalId=null),this.isPaused=!1,t&&this.dispatchEvent("stop")}}])&&o(t.prototype,e),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e}();document.addEventListener("DOMContentLoaded",(function(){document.querySelectorAll("[data-swe]").forEach((function(t){new a(t)}))}));const s=a;return"undefined"!=typeof window&&(window.SWE=a),e.default})()));
1
+ !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SWE=e():t.SWE=e()}(this,()=>(()=>{"use strict";var t={d:(e,n)=>{for(var i in n)t.o(n,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:n[i]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},e={};function n(t){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},n(t)}function i(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=Array(e);n<e;n++)i[n]=t[n];return i}function o(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(t,r(i.key),i)}}function r(t){var e=function(t){if("object"!=n(t)||!t)return t;var e=t[Symbol.toPrimitive];if(void 0!==e){var i=e.call(t,"string");if("object"!=n(i))return i;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(t)}(t);return"symbol"==n(e)?e:e+""}t.d(e,{default:()=>s});var a=function(){return t=function t(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.element=e,this.options={autostart:void 0===n.autostart||n.autostart,duration:n.duration,countdownEnd:n.countdownEnd,onTick:n.onTick,onEnd:n.onEnd,onStart:n.onStart,onPause:n.onPause,onResume:n.onResume,onReset:n.onReset,onStop:n.onStop},this.intervalId=null,this.isPaused=!1,this.startTime=null,this.pausedTimeRemaining=null,this.endTime=null,this.initialize()},(e=[{key:"initialize",value:function(){if(!(this.element&&this.element instanceof HTMLElement))throw new Error("SWE: Invalid element provided");if(this.mode=this.determineMode(),this.setupElements(),this.options.autostart)this.start();else if("countdown-duration"===this.mode&&this.options.duration)this.updateDisplay(1e3*this.options.duration);else if("countdown-end"===this.mode&&this.options.countdownEnd){var t=new Date(this.options.countdownEnd),e=new Date,n=t.getTime()-e.getTime();this.updateDisplay(n>0?n:0)}}},{key:"determineMode",value:function(){if(this.options.countdownEnd)return"countdown-end";if(this.options.duration)return"countdown-duration";if(this.element.hasAttribute("data-swe-countdown-end"))return"countdown-end";if(this.element.hasAttribute("data-swe-countdown-duration"))return"countdown-duration";if(this.element.hasAttribute("data-swe-current"))return"current";if(this.options.countdownEnd)return"countdown-end";throw new Error("SWE: Invalid mode - missing required configuration")}},{key:"setupElements",value:function(){this.elements={year:this.element.querySelector("[data-swe-year]"),month:this.element.querySelector("[data-swe-month]"),day:this.element.querySelector("[data-swe-day]"),hour:this.element.querySelector("[data-swe-hour]"),minute:this.element.querySelector("[data-swe-minute]"),second:this.element.querySelector("[data-swe-second]")}}},{key:"start",value:function(){if(!this.intervalId){switch(this.isPaused=!1,this.startTime=Date.now(),this.mode){case"countdown-end":this.startCountdownToDate();break;case"countdown-duration":this.startCountdownDuration();break;case"current":this.startCurrentTime()}this.dispatchEvent("start")}}},{key:"startCountdownToDate",value:function(){var t=new Date(this.options.countdownEnd||this.element.getAttribute("data-swe-countdown-end"));if(isNaN(t.getTime()))throw new Error("SWE: Invalid end date format");this.endTime=t.getTime(),this.updateTimer()}},{key:"startCountdownDuration",value:function(){var t=this.options.duration||parseInt(this.element.getAttribute("data-swe-countdown-duration"),10);if(isNaN(t)||t<0)throw new Error("SWE: Invalid duration");null!==this.pausedTimeRemaining?this.endTime=Date.now()+this.pausedTimeRemaining:this.endTime=Date.now()+1e3*t,this.updateTimer()}},{key:"updateTimer",value:function(){var t=this,e=function(){var e=Date.now(),n=t.endTime-e;return n<=0?(t.stop(),t.updateDisplay(0),t.dispatchEvent("end"),!1):(t.updateDisplay(n),t.dispatchEvent("tick"),!0)};e()&&(this.intervalId=setInterval(e,1e3))}},{key:"startCurrentTime",value:function(){var t=this,e=function(){var e=new Date;t.updateCurrentTimeDisplay(e),t.dispatchEvent("tick")};e(),this.intervalId=setInterval(e,1e3)}},{key:"updateDisplay",value:function(t){var e=this,n=this.calculateTimeUnits(t);Object.entries(this.elements).forEach(function(t){var o=function(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=n){var i,o,r,a,s=[],u=!0,d=!1;try{if(r=(n=n.call(t)).next,0===e){if(Object(n)!==n)return;u=!1}else for(;!(u=(i=r.call(n)).done)&&(s.push(i.value),s.length!==e);u=!0);}catch(t){d=!0,o=t}finally{try{if(!u&&null!=n.return&&(a=n.return(),Object(a)!==a))return}finally{if(d)throw o}}return s}}(t,e)||function(t,e){if(t){if("string"==typeof t)return i(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?i(t,e):void 0}}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}(t,2),r=o[0],a=o[1];a&&e.updateElement(a,n[r])})}},{key:"updateCurrentTimeDisplay",value:function(t){this.elements.hour&&this.updateElement(this.elements.hour,t.getHours()),this.elements.minute&&this.updateElement(this.elements.minute,t.getMinutes()),this.elements.second&&this.updateElement(this.elements.second,t.getSeconds())}},{key:"updateElement",value:function(t,e){var n=t.getAttribute("data-swe-format");t.textContent=this.formatValue(e,n)}},{key:"formatValue",value:function(t,e){return e?t.toString().padStart(e.length,"0"):t.toString()}},{key:"calculateTimeUnits",value:function(t){var e=Math.floor(t/1e3),n=Math.floor(e/60),i=Math.floor(n/60),o=(Math.floor(i/24),new Date),r=new Date(o.getTime()+t),a=r.getFullYear()-o.getFullYear(),s=r.getMonth()-o.getMonth(),u=r.getDate()-o.getDate();return u<0&&(s--,u+=new Date(r.getFullYear(),r.getMonth(),0).getDate()),s<0&&(a--,s+=12),{year:a,month:s,day:u,hour:i%24,minute:n%60,second:e%60}}},{key:"dispatchEvent",value:function(t){var e="on".concat(t.charAt(0).toUpperCase()+t.slice(1));"function"==typeof this.options[e]&&this.options[e].call(this);var n=new CustomEvent("swe:".concat(t),{bubbles:!0,detail:{instance:this}});this.element.dispatchEvent(n)}},{key:"pause",value:function(){this.intervalId&&!this.isPaused&&(clearInterval(this.intervalId),this.intervalId=null,this.isPaused=!0,this.pausedTimeRemaining=this.endTime-Date.now(),this.dispatchEvent("pause"))}},{key:"resume",value:function(){if(this.isPaused){switch(this.isPaused=!1,this.startTime=Date.now(),this.mode){case"countdown-end":this.startCountdownToDate();break;case"countdown-duration":this.startCountdownDuration();break;case"current":this.startCurrentTime()}this.dispatchEvent("resume")}}},{key:"reset",value:function(){if(this.stop(),this.pausedTimeRemaining=null,this.endTime=null,"countdown-end"===this.mode&&this.options.countdownEnd){var t=new Date(this.options.countdownEnd),e=new Date,n=t.getTime()-e.getTime();this.updateDisplay(n>0?n:0)}else"countdown-duration"===this.mode&&this.options.duration&&this.updateDisplay(1e3*this.options.duration);this.dispatchEvent("reset"),this.options.autostart&&this.start()}},{key:"stop",value:function(){var t=null!==this.intervalId;this.intervalId&&(clearInterval(this.intervalId),this.intervalId=null),this.isPaused=!1,t&&this.dispatchEvent("stop")}}])&&o(t.prototype,e),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e}();document.addEventListener("DOMContentLoaded",function(){document.querySelectorAll("[data-swe]").forEach(function(t){new a(t)})});const s=a;return"undefined"!=typeof window&&(window.SWE=a),e.default})());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "senangwebs-epoch",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Lightweight JavaScript library for creating dynamic countdown timers and time displays.",
5
5
  "main": "dist/swe.js",
6
6
  "module": "dist/swe.js",
@@ -10,7 +10,7 @@
10
10
  "scripts": {
11
11
  "build": "webpack --mode production",
12
12
  "dev": "webpack --mode development --watch",
13
- "test": "echo \"Error: no test specified\" && exit 1"
13
+ "test": "node --test"
14
14
  },
15
15
  "keywords": ["countdown", "timer", "time", "epoch", "javascript", "date", "clock"],
16
16
  "author": "a-hakim",
package/src/js/swe.js CHANGED
@@ -140,28 +140,27 @@ class SWE {
140
140
  this.updateTimer();
141
141
  }
142
142
 
143
- updateTimer() {
144
- const update = () => {
145
- const now = Date.now();
146
- const remaining = this.endTime - now;
147
-
148
- if (remaining <= 0) {
149
- this.stop();
150
- this.updateDisplay(0);
151
- this.dispatchEvent('end');
152
- return;
153
- }
154
-
155
- this.updateDisplay(remaining);
156
- this.dispatchEvent('tick');
157
- };
158
-
159
- // Initial update
160
- update();
161
-
162
- // Set interval for subsequent updates
163
- this.intervalId = setInterval(update, 1000);
164
- }
143
+ updateTimer() {
144
+ const update = () => {
145
+ const now = Date.now();
146
+ const remaining = this.endTime - now;
147
+
148
+ if (remaining <= 0) {
149
+ this.stop();
150
+ this.updateDisplay(0);
151
+ this.dispatchEvent('end');
152
+ return false;
153
+ }
154
+
155
+ this.updateDisplay(remaining);
156
+ this.dispatchEvent('tick');
157
+ return true;
158
+ };
159
+
160
+ if (update()) {
161
+ this.intervalId = setInterval(update, 1000);
162
+ }
163
+ }
165
164
 
166
165
  startCurrentTime() {
167
166
  const update = () => {
@@ -336,4 +335,4 @@ export default SWE;
336
335
 
337
336
  if (typeof window !== 'undefined') {
338
337
  window.SWE = SWE;
339
- }
338
+ }
@@ -0,0 +1,95 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+
4
+ class FakeHTMLElement {
5
+ constructor(attributes = {}) {
6
+ this.attributes = attributes;
7
+ this.events = [];
8
+ this.children = {};
9
+ }
10
+
11
+ hasAttribute(name) {
12
+ return Object.prototype.hasOwnProperty.call(this.attributes, name);
13
+ }
14
+
15
+ getAttribute(name) {
16
+ return this.attributes[name] ?? null;
17
+ }
18
+
19
+ querySelector(selector) {
20
+ return this.children[selector] ?? null;
21
+ }
22
+
23
+ dispatchEvent(event) {
24
+ this.events.push(event.type);
25
+ }
26
+ }
27
+
28
+ global.HTMLElement = FakeHTMLElement;
29
+ global.CustomEvent = class CustomEvent {
30
+ constructor(type, options) {
31
+ this.type = type;
32
+ this.detail = options.detail;
33
+ this.bubbles = options.bubbles;
34
+ }
35
+ };
36
+ global.document = {
37
+ addEventListener() {},
38
+ querySelectorAll() {
39
+ return [];
40
+ }
41
+ };
42
+
43
+ const SWE = require('../dist/swe.js');
44
+
45
+ test('expired countdown ends once without starting an interval', () => {
46
+ const element = new FakeHTMLElement({
47
+ 'data-swe-countdown-end': '2000-01-01T00:00:00Z'
48
+ });
49
+ let endCount = 0;
50
+ let intervalCount = 0;
51
+ const originalSetInterval = global.setInterval;
52
+
53
+ global.setInterval = () => {
54
+ intervalCount += 1;
55
+ return 1;
56
+ };
57
+
58
+ try {
59
+ const timer = new SWE(element, {
60
+ onEnd() {
61
+ endCount += 1;
62
+ }
63
+ });
64
+
65
+ assert.equal(timer.intervalId, null);
66
+ assert.equal(intervalCount, 0);
67
+ assert.equal(endCount, 1);
68
+ assert.equal(element.events.filter(event => event === 'swe:end').length, 1);
69
+ } finally {
70
+ global.setInterval = originalSetInterval;
71
+ }
72
+ });
73
+
74
+ test('zero-duration countdown ends without starting an interval', () => {
75
+ const element = new FakeHTMLElement({
76
+ 'data-swe-countdown-duration': '0'
77
+ });
78
+ let intervalCount = 0;
79
+ const originalSetInterval = global.setInterval;
80
+
81
+ global.setInterval = () => {
82
+ intervalCount += 1;
83
+ return 1;
84
+ };
85
+
86
+ try {
87
+ const timer = new SWE(element);
88
+
89
+ assert.equal(timer.intervalId, null);
90
+ assert.equal(intervalCount, 0);
91
+ assert.equal(element.events.filter(event => event === 'swe:end').length, 1);
92
+ } finally {
93
+ global.setInterval = originalSetInterval;
94
+ }
95
+ });