@hkdigital/lib-core 0.5.45 → 0.5.46
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.
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
* - State-to-route mapping and sync
|
|
10
10
|
* - Data properties for business/domain state
|
|
11
11
|
* - Visited states tracking
|
|
12
|
+
* - onEnter hooks with abort/complete handlers for animations
|
|
12
13
|
*
|
|
13
14
|
* Basic usage:
|
|
14
15
|
* ```javascript
|
|
15
|
-
* const machine =
|
|
16
|
+
* const machine = new PageMachine({
|
|
17
|
+
* initialState: STATE_START,
|
|
18
|
+
* routeMap: {
|
|
19
|
+
* [STATE_START]: '/intro/start',
|
|
20
|
+
* [STATE_PROFILE]: '/intro/profile'
|
|
21
|
+
* }
|
|
22
|
+
* });
|
|
16
23
|
*
|
|
17
24
|
* // Sync machine state with URL changes
|
|
18
25
|
* $effect(() => {
|
|
@@ -20,54 +27,73 @@
|
|
|
20
27
|
* });
|
|
21
28
|
* ```
|
|
22
29
|
*
|
|
23
|
-
* With
|
|
30
|
+
* With onEnter hooks (for animations):
|
|
24
31
|
* ```javascript
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* const machine = new PageMachine({
|
|
33
|
+
* initialState: STATE_ANIMATE,
|
|
34
|
+
* routeMap: {
|
|
35
|
+
* [STATE_ANIMATE]: '/game/animate',
|
|
36
|
+
* [STATE_PLAY]: '/game/play'
|
|
37
|
+
* },
|
|
38
|
+
* onEnterHooks: {
|
|
39
|
+
* [STATE_ANIMATE]: (done) => {
|
|
40
|
+
* const animation = playAnimation(1000);
|
|
41
|
+
* animation.finished.then(() => done(STATE_PLAY));
|
|
32
42
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
43
|
+
* return {
|
|
44
|
+
* abort: () => animation.cancel(),
|
|
45
|
+
* complete: () => animation.finish()
|
|
46
|
+
* };
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
* });
|
|
37
50
|
*
|
|
38
|
-
* //
|
|
39
|
-
* machine.
|
|
51
|
+
* // Fast-forward animation
|
|
52
|
+
* machine.completeTransitions();
|
|
40
53
|
*
|
|
41
|
-
* //
|
|
42
|
-
*
|
|
43
|
-
* // User has seen profile page before
|
|
44
|
-
* }
|
|
54
|
+
* // Cancel animation
|
|
55
|
+
* machine.abortTransitions();
|
|
45
56
|
* ```
|
|
46
57
|
*/
|
|
47
58
|
export default class PageMachine {
|
|
48
59
|
/**
|
|
49
60
|
* Constructor
|
|
50
61
|
*
|
|
51
|
-
* @param {
|
|
52
|
-
* @param {
|
|
53
|
-
* @param {Record<string,
|
|
62
|
+
* @param {Object} config - Configuration object
|
|
63
|
+
* @param {string} config.initialState - Initial state name
|
|
64
|
+
* @param {Record<string, string>} [config.routeMap={}] - Map of states to route paths
|
|
65
|
+
* @param {Record<string, any>} [config.initialData={}] - Initial data properties (from server)
|
|
66
|
+
* @param {Record<string, Function>} [config.onEnterHooks={}] - Map of states to onEnter hook functions
|
|
54
67
|
*
|
|
55
68
|
* @example
|
|
56
69
|
* ```javascript
|
|
57
|
-
* const
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
70
|
+
* const machine = new PageMachine({
|
|
71
|
+
* initialState: STATE_START,
|
|
72
|
+
* routeMap: {
|
|
73
|
+
* [STATE_START]: '/intro/start',
|
|
74
|
+
* [STATE_ANIMATE]: '/intro/animate'
|
|
75
|
+
* },
|
|
76
|
+
* initialData: {
|
|
77
|
+
* INTRO_COMPLETED: false
|
|
78
|
+
* },
|
|
79
|
+
* onEnterHooks: {
|
|
80
|
+
* [STATE_ANIMATE]: (done) => {
|
|
81
|
+
* setTimeout(() => done(STATE_START), 1000);
|
|
82
|
+
* return {
|
|
83
|
+
* abort: () => clearTimeout(...),
|
|
84
|
+
* complete: () => done(STATE_START)
|
|
85
|
+
* };
|
|
86
|
+
* }
|
|
87
|
+
* }
|
|
88
|
+
* });
|
|
68
89
|
* ```
|
|
69
90
|
*/
|
|
70
|
-
constructor(initialState
|
|
91
|
+
constructor({ initialState, routeMap, initialData, onEnterHooks }: {
|
|
92
|
+
initialState: string;
|
|
93
|
+
routeMap?: Record<string, string> | undefined;
|
|
94
|
+
initialData?: Record<string, any> | undefined;
|
|
95
|
+
onEnterHooks?: Record<string, Function> | undefined;
|
|
96
|
+
});
|
|
71
97
|
/**
|
|
72
98
|
* Synchronize machine state with URL path
|
|
73
99
|
*
|
|
@@ -81,10 +107,11 @@ export default class PageMachine {
|
|
|
81
107
|
syncFromPath(currentPath: string): boolean;
|
|
82
108
|
/**
|
|
83
109
|
* Set the current state directly
|
|
110
|
+
* Handles onEnter hooks and auto-transitions
|
|
84
111
|
*
|
|
85
112
|
* @param {string} newState - Target state
|
|
86
113
|
*/
|
|
87
|
-
setState(newState: string): void
|
|
114
|
+
setState(newState: string): Promise<void>;
|
|
88
115
|
/**
|
|
89
116
|
* Get route path for a given state
|
|
90
117
|
*
|
|
@@ -191,5 +218,53 @@ export default class PageMachine {
|
|
|
191
218
|
* Useful for testing or resetting experience
|
|
192
219
|
*/
|
|
193
220
|
resetVisitedStates(): void;
|
|
221
|
+
/**
|
|
222
|
+
* Abort current state's transitions
|
|
223
|
+
* Cancels animations/operations immediately (incomplete state)
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```javascript
|
|
227
|
+
* // User clicks "Cancel" button
|
|
228
|
+
* machine.abortTransitions();
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
abortTransitions(): void;
|
|
232
|
+
/**
|
|
233
|
+
* Complete current state's transitions immediately
|
|
234
|
+
* Fast-forwards animations/operations to completion (complete state)
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```javascript
|
|
238
|
+
* // User clicks "Skip" or "Next" button
|
|
239
|
+
* machine.completeTransitions();
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
completeTransitions(): void;
|
|
243
|
+
/**
|
|
244
|
+
* Check if current state has transitions that can be completed
|
|
245
|
+
*
|
|
246
|
+
* @returns {boolean} True if completeTransitions() can be called
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```svelte
|
|
250
|
+
* {#if machine.canCompleteTransitions}
|
|
251
|
+
* <button onclick={() => machine.completeTransitions()}>Skip</button>
|
|
252
|
+
* {/if}
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
get canCompleteTransitions(): boolean;
|
|
256
|
+
/**
|
|
257
|
+
* Check if current state has transitions that can be aborted
|
|
258
|
+
*
|
|
259
|
+
* @returns {boolean} True if abortTransitions() can be called
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```svelte
|
|
263
|
+
* {#if machine.canAbortTransitions}
|
|
264
|
+
* <button onclick={() => machine.abortTransitions()}>Cancel</button>
|
|
265
|
+
* {/if}
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
get canAbortTransitions(): boolean;
|
|
194
269
|
#private;
|
|
195
270
|
}
|
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
* - State-to-route mapping and sync
|
|
10
10
|
* - Data properties for business/domain state
|
|
11
11
|
* - Visited states tracking
|
|
12
|
+
* - onEnter hooks with abort/complete handlers for animations
|
|
12
13
|
*
|
|
13
14
|
* Basic usage:
|
|
14
15
|
* ```javascript
|
|
15
|
-
* const machine =
|
|
16
|
+
* const machine = new PageMachine({
|
|
17
|
+
* initialState: STATE_START,
|
|
18
|
+
* routeMap: {
|
|
19
|
+
* [STATE_START]: '/intro/start',
|
|
20
|
+
* [STATE_PROFILE]: '/intro/profile'
|
|
21
|
+
* }
|
|
22
|
+
* });
|
|
16
23
|
*
|
|
17
24
|
* // Sync machine state with URL changes
|
|
18
25
|
* $effect(() => {
|
|
@@ -20,28 +27,32 @@
|
|
|
20
27
|
* });
|
|
21
28
|
* ```
|
|
22
29
|
*
|
|
23
|
-
* With
|
|
30
|
+
* With onEnter hooks (for animations):
|
|
24
31
|
* ```javascript
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* const machine = new PageMachine({
|
|
33
|
+
* initialState: STATE_ANIMATE,
|
|
34
|
+
* routeMap: {
|
|
35
|
+
* [STATE_ANIMATE]: '/game/animate',
|
|
36
|
+
* [STATE_PLAY]: '/game/play'
|
|
37
|
+
* },
|
|
38
|
+
* onEnterHooks: {
|
|
39
|
+
* [STATE_ANIMATE]: (done) => {
|
|
40
|
+
* const animation = playAnimation(1000);
|
|
41
|
+
* animation.finished.then(() => done(STATE_PLAY));
|
|
32
42
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
43
|
+
* return {
|
|
44
|
+
* abort: () => animation.cancel(),
|
|
45
|
+
* complete: () => animation.finish()
|
|
46
|
+
* };
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
* });
|
|
37
50
|
*
|
|
38
|
-
* //
|
|
39
|
-
* machine.
|
|
51
|
+
* // Fast-forward animation
|
|
52
|
+
* machine.completeTransitions();
|
|
40
53
|
*
|
|
41
|
-
* //
|
|
42
|
-
*
|
|
43
|
-
* // User has seen profile page before
|
|
44
|
-
* }
|
|
54
|
+
* // Cancel animation
|
|
55
|
+
* machine.abortTransitions();
|
|
45
56
|
* ```
|
|
46
57
|
*/
|
|
47
58
|
export default class PageMachine {
|
|
@@ -85,32 +96,71 @@ export default class PageMachine {
|
|
|
85
96
|
*/
|
|
86
97
|
#revision = $state(0);
|
|
87
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Map of state names to onEnter hook configurations
|
|
101
|
+
* @type {Record<string, {onEnter: Function}>}
|
|
102
|
+
*/
|
|
103
|
+
#onEnterHooks = {};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Current state's onEnter handler (abort/complete functions)
|
|
107
|
+
* @type {{abort?: Function, complete?: Function} | null}
|
|
108
|
+
*/
|
|
109
|
+
#currentOnEnterHandler = null;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Current state's done callback
|
|
113
|
+
* @type {Function | null}
|
|
114
|
+
*/
|
|
115
|
+
#currentOnEnterDone = null;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Flag to prevent concurrent state transitions
|
|
119
|
+
* @type {boolean}
|
|
120
|
+
*/
|
|
121
|
+
#isTransitioning = false;
|
|
122
|
+
|
|
88
123
|
/**
|
|
89
124
|
* Constructor
|
|
90
125
|
*
|
|
91
|
-
* @param {
|
|
92
|
-
* @param {
|
|
93
|
-
* @param {Record<string,
|
|
126
|
+
* @param {Object} config - Configuration object
|
|
127
|
+
* @param {string} config.initialState - Initial state name
|
|
128
|
+
* @param {Record<string, string>} [config.routeMap={}] - Map of states to route paths
|
|
129
|
+
* @param {Record<string, any>} [config.initialData={}] - Initial data properties (from server)
|
|
130
|
+
* @param {Record<string, Function>} [config.onEnterHooks={}] - Map of states to onEnter hook functions
|
|
94
131
|
*
|
|
95
132
|
* @example
|
|
96
133
|
* ```javascript
|
|
97
|
-
* const
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
134
|
+
* const machine = new PageMachine({
|
|
135
|
+
* initialState: STATE_START,
|
|
136
|
+
* routeMap: {
|
|
137
|
+
* [STATE_START]: '/intro/start',
|
|
138
|
+
* [STATE_ANIMATE]: '/intro/animate'
|
|
139
|
+
* },
|
|
140
|
+
* initialData: {
|
|
141
|
+
* INTRO_COMPLETED: false
|
|
142
|
+
* },
|
|
143
|
+
* onEnterHooks: {
|
|
144
|
+
* [STATE_ANIMATE]: (done) => {
|
|
145
|
+
* setTimeout(() => done(STATE_START), 1000);
|
|
146
|
+
* return {
|
|
147
|
+
* abort: () => clearTimeout(...),
|
|
148
|
+
* complete: () => done(STATE_START)
|
|
149
|
+
* };
|
|
150
|
+
* }
|
|
151
|
+
* }
|
|
152
|
+
* });
|
|
108
153
|
* ```
|
|
109
154
|
*/
|
|
110
|
-
constructor(initialState, routeMap = {}, initialData = {}) {
|
|
155
|
+
constructor({ initialState, routeMap = {}, initialData = {}, onEnterHooks = {} }) {
|
|
156
|
+
if (!initialState) {
|
|
157
|
+
throw new Error('PageMachine requires initialState parameter');
|
|
158
|
+
}
|
|
159
|
+
|
|
111
160
|
this.#current = initialState;
|
|
112
161
|
this.#routeMap = routeMap;
|
|
113
162
|
this.#data = initialData;
|
|
163
|
+
this.#onEnterHooks = this.#normalizeOnEnterHooks(onEnterHooks);
|
|
114
164
|
|
|
115
165
|
// Build reverse map (path -> state)
|
|
116
166
|
for (const [state, path] of Object.entries(routeMap)) {
|
|
@@ -121,6 +171,29 @@ export default class PageMachine {
|
|
|
121
171
|
this.#visitedStates.add(initialState);
|
|
122
172
|
}
|
|
123
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Normalize onEnterHooks to ensure consistent format
|
|
176
|
+
* Converts function to {onEnter: function} object
|
|
177
|
+
*
|
|
178
|
+
* @param {Record<string, Function|Object>} hooks - Raw hooks configuration
|
|
179
|
+
* @returns {Record<string, {onEnter: Function}>} Normalized hooks
|
|
180
|
+
*/
|
|
181
|
+
#normalizeOnEnterHooks(hooks) {
|
|
182
|
+
const normalized = {};
|
|
183
|
+
|
|
184
|
+
for (const [state, hook] of Object.entries(hooks)) {
|
|
185
|
+
if (typeof hook === 'function') {
|
|
186
|
+
// Simple function -> wrap in object
|
|
187
|
+
normalized[state] = { onEnter: hook };
|
|
188
|
+
} else if (hook && typeof hook === 'object' && hook.onEnter) {
|
|
189
|
+
// Already an object with onEnter
|
|
190
|
+
normalized[state] = hook;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return normalized;
|
|
195
|
+
}
|
|
196
|
+
|
|
124
197
|
/**
|
|
125
198
|
* Synchronize machine state with URL path
|
|
126
199
|
*
|
|
@@ -146,13 +219,66 @@ export default class PageMachine {
|
|
|
146
219
|
|
|
147
220
|
/**
|
|
148
221
|
* Set the current state directly
|
|
222
|
+
* Handles onEnter hooks and auto-transitions
|
|
149
223
|
*
|
|
150
224
|
* @param {string} newState - Target state
|
|
151
225
|
*/
|
|
152
|
-
setState(newState) {
|
|
153
|
-
if (newState
|
|
154
|
-
|
|
226
|
+
async setState(newState) {
|
|
227
|
+
if (newState === this.#current || this.#isTransitioning) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Abort previous state's onEnter handler
|
|
232
|
+
if (this.#currentOnEnterHandler?.abort) {
|
|
233
|
+
this.#currentOnEnterHandler.abort();
|
|
155
234
|
}
|
|
235
|
+
this.#currentOnEnterHandler = null;
|
|
236
|
+
this.#currentOnEnterDone = null;
|
|
237
|
+
|
|
238
|
+
this.#isTransitioning = true;
|
|
239
|
+
this.#current = newState;
|
|
240
|
+
this.#visitedStates.add(newState);
|
|
241
|
+
|
|
242
|
+
// Check if this state has an onEnter hook
|
|
243
|
+
const hookConfig = this.#onEnterHooks[newState];
|
|
244
|
+
if (hookConfig?.onEnter) {
|
|
245
|
+
// Create done callback for auto-transition
|
|
246
|
+
let doneCalled = false;
|
|
247
|
+
const done = (nextState) => {
|
|
248
|
+
if (!doneCalled && nextState && nextState !== newState) {
|
|
249
|
+
doneCalled = true;
|
|
250
|
+
this.#isTransitioning = false;
|
|
251
|
+
this.setState(nextState);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
this.#currentOnEnterDone = done;
|
|
256
|
+
|
|
257
|
+
// Call the onEnter hook
|
|
258
|
+
try {
|
|
259
|
+
const handler = hookConfig.onEnter(done);
|
|
260
|
+
|
|
261
|
+
// Store abort/complete handlers if provided
|
|
262
|
+
if (handler && typeof handler === 'object') {
|
|
263
|
+
if (handler.abort || handler.complete) {
|
|
264
|
+
this.#currentOnEnterHandler = {
|
|
265
|
+
abort: handler.abort,
|
|
266
|
+
complete: handler.complete
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// If hook returned a promise, await it
|
|
272
|
+
if (handler?.then) {
|
|
273
|
+
await handler;
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error(`Error in onEnter hook for state ${newState}:`, error);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
this.#isTransitioning = false;
|
|
281
|
+
this.#revision++;
|
|
156
282
|
}
|
|
157
283
|
|
|
158
284
|
/**
|
|
@@ -334,4 +460,78 @@ export default class PageMachine {
|
|
|
334
460
|
this.#visitedStates.add(this.#current);
|
|
335
461
|
this.#revision++;
|
|
336
462
|
}
|
|
463
|
+
|
|
464
|
+
/* ===== Transition Control Methods ===== */
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Abort current state's transitions
|
|
468
|
+
* Cancels animations/operations immediately (incomplete state)
|
|
469
|
+
*
|
|
470
|
+
* @example
|
|
471
|
+
* ```javascript
|
|
472
|
+
* // User clicks "Cancel" button
|
|
473
|
+
* machine.abortTransitions();
|
|
474
|
+
* ```
|
|
475
|
+
*/
|
|
476
|
+
abortTransitions() {
|
|
477
|
+
if (this.#currentOnEnterHandler?.abort) {
|
|
478
|
+
this.#currentOnEnterHandler.abort();
|
|
479
|
+
this.#currentOnEnterHandler = null;
|
|
480
|
+
this.#currentOnEnterDone = null;
|
|
481
|
+
this.#revision++;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Complete current state's transitions immediately
|
|
487
|
+
* Fast-forwards animations/operations to completion (complete state)
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```javascript
|
|
491
|
+
* // User clicks "Skip" or "Next" button
|
|
492
|
+
* machine.completeTransitions();
|
|
493
|
+
* ```
|
|
494
|
+
*/
|
|
495
|
+
completeTransitions() {
|
|
496
|
+
if (this.#currentOnEnterHandler?.complete) {
|
|
497
|
+
this.#currentOnEnterHandler.complete();
|
|
498
|
+
this.#currentOnEnterHandler = null;
|
|
499
|
+
this.#currentOnEnterDone = null;
|
|
500
|
+
this.#revision++;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Check if current state has transitions that can be completed
|
|
506
|
+
*
|
|
507
|
+
* @returns {boolean} True if completeTransitions() can be called
|
|
508
|
+
*
|
|
509
|
+
* @example
|
|
510
|
+
* ```svelte
|
|
511
|
+
* {#if machine.canCompleteTransitions}
|
|
512
|
+
* <button onclick={() => machine.completeTransitions()}>Skip</button>
|
|
513
|
+
* {/if}
|
|
514
|
+
* ```
|
|
515
|
+
*/
|
|
516
|
+
get canCompleteTransitions() {
|
|
517
|
+
this.#revision; // Ensure reactivity
|
|
518
|
+
return !!this.#currentOnEnterHandler?.complete;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Check if current state has transitions that can be aborted
|
|
523
|
+
*
|
|
524
|
+
* @returns {boolean} True if abortTransitions() can be called
|
|
525
|
+
*
|
|
526
|
+
* @example
|
|
527
|
+
* ```svelte
|
|
528
|
+
* {#if machine.canAbortTransitions}
|
|
529
|
+
* <button onclick={() => machine.abortTransitions()}>Cancel</button>
|
|
530
|
+
* {/if}
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
533
|
+
get canAbortTransitions() {
|
|
534
|
+
this.#revision; // Ensure reactivity
|
|
535
|
+
return !!this.#currentOnEnterHandler?.abort;
|
|
536
|
+
}
|
|
337
537
|
}
|
package/dist/util/time/index.js
CHANGED
|
@@ -185,12 +185,16 @@ export function getWeekNumber(dateOrTimestamp) {
|
|
|
185
185
|
//
|
|
186
186
|
// Create a copy of this date object
|
|
187
187
|
//
|
|
188
|
-
const target = new Date(
|
|
188
|
+
const target = new Date(Date.UTC(
|
|
189
|
+
date.getUTCFullYear(),
|
|
190
|
+
date.getUTCMonth(),
|
|
191
|
+
date.getUTCDate()
|
|
192
|
+
));
|
|
189
193
|
|
|
190
194
|
//
|
|
191
195
|
// ISO week date weeks start on Monday, so correct the day number
|
|
192
196
|
//
|
|
193
|
-
const dayNumber = (date.
|
|
197
|
+
const dayNumber = (date.getUTCDay() + 6) % 7;
|
|
194
198
|
|
|
195
199
|
//
|
|
196
200
|
// ISO 8601 states that week 1 is the week with the first Thursday
|
|
@@ -198,22 +202,29 @@ export function getWeekNumber(dateOrTimestamp) {
|
|
|
198
202
|
//
|
|
199
203
|
// Set the target date to the Thursday in the target week
|
|
200
204
|
//
|
|
201
|
-
target.
|
|
205
|
+
target.setUTCDate(target.getUTCDate() - dayNumber + 3);
|
|
202
206
|
|
|
203
207
|
//
|
|
204
208
|
// Store the millisecond value of the target date
|
|
205
209
|
//
|
|
206
210
|
const firstThursday = target.valueOf();
|
|
207
211
|
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
212
|
+
//
|
|
213
|
+
// Get the year of the Thursday in the target week
|
|
214
|
+
// (This is important for dates near year boundaries)
|
|
215
|
+
//
|
|
216
|
+
const yearOfThursday = target.getUTCFullYear();
|
|
217
|
+
|
|
218
|
+
// Set the target to the first Thursday of that year
|
|
219
|
+
// First, set the target to January 1st of that year
|
|
220
|
+
target.setUTCFullYear(yearOfThursday);
|
|
221
|
+
target.setUTCMonth(0, 1);
|
|
211
222
|
|
|
212
223
|
//
|
|
213
224
|
// Not a Thursday? Correct the date to the next Thursday
|
|
214
225
|
//
|
|
215
|
-
if (target.
|
|
216
|
-
target.
|
|
226
|
+
if (target.getUTCDay() !== 4) {
|
|
227
|
+
target.setUTCMonth(0, 1 + ((4 - target.getUTCDay() + 7) % 7));
|
|
217
228
|
}
|
|
218
229
|
|
|
219
230
|
//
|