PageSourceSearch

https://funasapo.com/wp-content/themes/funasapo_station/js/monthly-program-slider.js?ver=1.0.0

js funasapo.com collected 2026-10-02 00:06:44 UTC 17,587 bytes, 1,639 lines download raw bytes

1document.addEventListener(
2	'DOMContentLoaded',
3	function () {
4
5		'use strict';
6
7
8		/*
9		 * =====================================================
10		 * スライダー
11		 * =====================================================
12		 */
13
14		const slider =
15			document.querySelector(
16				'.program__slider'
17			);
18
19
20		if (!slider) {
21			return;
22		}
23
24
25		const windowElement =
26			slider.querySelector(
27				'.program__slider__window'
28			);
29
30
31		const track =
32			slider.querySelector(
33				'.program__slider__track'
34			);
35
36
37		const items =
38			Array.from(
39				slider.querySelectorAll(
40					'.program__slider__item'
41				)
42			);
43
44
45		const prev =
46			slider.querySelector(
47				'.program__arrow--prev'
48			);
49
50
51		const next =
52			slider.querySelector(
53				'.program__arrow--next'
54			);
55
56
57		if (
58			!windowElement ||
59			!track ||
60			!prev ||
61			!next ||
62			!items.length
63		) {
64			return;
65		}
66
67
68
69		/*
70		 * =====================================================
71		 * 初期表示
72		 * =====================================================
73		 *
74		 * 1. 今月
75		 * 2. 今月がなければ直近の過去月
76		 * 3. 過去月もなければ一番近い未来月
77		 */
78
79
80		let current =
81			items.findIndex(
82				function (item) {
83
84					return (
85						item.dataset.current ===
86						'true'
87					);
88
89				}
90			);
91
92
93		if (current === -1) {
94
95
96			const currentMonth =
97				slider.dataset.currentMonth ||
98				'';
99
100
101			let latestPastIndex =
102				-1;
103
104
105			items.forEach(
106				function (
107					item,
108					index
109				) {
110
111
112					const month =
113						item.dataset.month ||
114						'';
115
116
117					if (
118						! /^\d{4}-(0[1-9]|1[0-2])$/
119							.test(month)
120					) {
121						return;
122					}
123
124
125					if (
126						currentMonth &&
127						month <= currentMonth
128					) {
129
130						latestPastIndex =
131							index;
132
133					}
134
135				}
136			);
137
138
139			current =
140				latestPastIndex !== -1
141					? latestPastIndex
142					: 0;
143
144		}
145
146
147
148		/*
149		 * =====================================================
150		 * スライダー更新
151		 * =====================================================
152		 */
153
154
155		function updateSlider() {
156
157
158			const itemWidth =
159				items[0].offsetWidth;
160
161
162			const windowWidth =
163				windowElement.offsetWidth;
164
165
166			if (
167				!itemWidth ||
168				!windowWidth
169			) {
170				return;
171			}
172
173
174			const position =
175				(windowWidth / 2) -
176				(itemWidth / 2) -
177				(current * itemWidth);
178
179
180			track.style.transform =
181				'translate3d(' +
182				position +
183				'px, 0, 0)';
184
185
186			items.forEach(
187				function (
188					item,
189					index
190				) {
191
192					item.classList.toggle(
193						'is-active',
194						index === current
195					);
196
197				}
198			);
199
200
201			prev.disabled =
202				current === 0;
203
204
205			next.disabled =
206				current ===
207				items.length - 1;
208
209		}
210
211
212
213		/*
214		 * 前月
215		 */
216		prev.addEventListener(
217			'click',
218			function () {
219
220				if (current <= 0) {
221					return;
222				}
223
224
225				current--;
226
227
228				updateSlider();
229
230			}
231		);
232
233
234
235		/*
236		 * 次月
237		 */
238		next.addEventListener(
239			'click',
240			function () {
241
242				if (
243					current >=
244					items.length - 1
245				) {
246					return;
247				}
248
249
250				current++;
251
252
253				updateSlider();
254
255			}
256		);
257
258
259
260		/*
261		 * =====================================================
262		 * 通常画面のスマホスワイプ
263		 * =====================================================
264		 */
265
266
267		let touchStartX = 0;
268		let touchStartY = 0;
269
270		let touchEndX = 0;
271		let touchEndY = 0;
272
273
274		const swipeThreshold =
275			40;
276
277
278
279		windowElement.addEventListener(
280			'touchstart',
281			function (event) {
282
283
284				if (
285					!event.changedTouches ||
286					!event.changedTouches.length
287				) {
288					return;
289				}
290
291
292				touchStartX =
293					event
294						.changedTouches[0]
295						.clientX;
296
297
298				touchStartY =
299					event
300						.changedTouches[0]
301						.clientY;
302
303			},
304			{
305				passive: true
306			}
307		);
308
309
310
311		windowElement.addEventListener(
312			'touchend',
313			function (event) {
314
315
316				if (
317					!event.changedTouches ||
318					!event.changedTouches.length
319				) {
320					return;
321				}
322
323
324				touchEndX =
325					event
326						.changedTouches[0]
327						.clientX;
328
329
330				touchEndY =
331					event
332						.changedTouches[0]
333						.clientY;
334
335
336				const distanceX =
337					touchEndX -
338						touchStartX;
339
340
341				const distanceY =
342					touchEndY -
343						touchStartY;
344
345
346				if (
347					Math.abs(distanceY) >
348					Math.abs(distanceX)
349				) {
350					return;
351				}
352
353
354				if (
355					Math.abs(distanceX) <
356					swipeThreshold
357				) {
358					return;
359				}
360
361
362				if (
363					distanceX < 0 &&
364					current <
365						items.length - 1
366				) {
367
368					current++;
369
370					updateSlider();
371
372					return;
373
374				}
375
376
377				if (
378					distanceX > 0 &&
379					current > 0
380				) {
381
382					current--;
383
384					updateSlider();
385
386				}
387
388			},
389			{
390				passive: true
391			}
392		);
393
394
395
396		/*
397		 * =====================================================
398		 * 拡大モーダル
399		 * =====================================================
400		 */
401
402
403		const modal =
404			document.querySelector(
405				'.programModal'
406			);
407
408
409		if (!modal) {
410
411			updateSlider();
412
413			return;
414
415		}
416
417
418
419		const modalImage =
420			modal.querySelector(
421				'.programModal__image'
422			);
423
424
425		const modalViewport =
426			modal.querySelector(
427				'.programModal__viewport'
428			);
429
430
431		const modalBg =
432			modal.querySelector(
433				'.programModal__bg'
434			);
435
436
437		const modalClose =
438			modal.querySelector(
439				'.programModal__close'
440			);
441
442
443		const zoomInButton =
444			modal.querySelector(
445				'.programModal__zoomIn'
446			);
447
448
449		const zoomOutButton =
450			modal.querySelector(
451				'.programModal__zoomOut'
452			);
453
454
455		const resetButton =
456			modal.querySelector(
457				'.programModal__reset'
458			);
459
460
461		const zoomValue =
462			modal.querySelector(
463				'.programModal__zoomValue'
464			);
465
466
467		if (
468			!modalImage ||
469			!modalViewport
470		) {
471
472			updateSlider();
473
474			return;
475
476		}
477
478
479
480		/*
481		 * =====================================================
482		 * 拡大率
483		 * =====================================================
484		 */
485
486
487		let zoom = 1;
488
489		let baseWidth = 0;
490
491
492		const minZoom = 0.5;
493
494		const maxZoom = 5;
495
496		const zoomStep = 0.25;
497
498
499		let lastFocusedElement =
500			null;
501
502
503
504		function clampZoom(value) {
505
506
507			return Math.min(
508				maxZoom,
509				Math.max(
510					minZoom,
511					value
512				)
513			);
514
515		}
516
517
518
519		/*
520		 * 拡大率変更
521		 */
522		function setZoom(
523			newZoom,
524			anchorX,
525			anchorY
526		) {
527
528
529			if (!baseWidth) {
530				return;
531			}
532
533
534			const oldZoom =
535				zoom;
536
537
538			newZoom =
539				clampZoom(
540					newZoom
541				);
542
543
544			if (
545				Math.abs(
546					newZoom -
547					oldZoom
548				) < 0.001
549			) {
550				return;
551			}
552
553
554			if (
555				typeof anchorX !==
556					'number'
557			) {
558
559				anchorX =
560					modalViewport
561						.clientWidth /
562						2;
563
564			}
565
566
567			if (
568				typeof anchorY !==
569					'number'
570			) {
571
572				anchorY =
573					modalViewport
574						.clientHeight /
575						2;
576
577			}
578
579
580			const contentX =
581				modalViewport.scrollLeft +
582				anchorX;
583
584
585			const contentY =
586				modalViewport.scrollTop +
587				anchorY;
588
589
590			const ratio =
591				newZoom /
592					oldZoom;
593
594
595			zoom =
596				newZoom;
597
598
599			/*
600			 * transformではなく
601			 * 実際の画像幅を変更する。
602			 */
603			modalImage.style.width =
604				(baseWidth * zoom) +
605					'px';
606
607
608			if (zoomValue) {
609
610				zoomValue.textContent =
611					Math.round(
612						zoom * 100
613					) +
614					'%';
615
616			}
617
618
619			if (zoomOutButton) {
620
621				zoomOutButton.disabled =
622					zoom <= minZoom;
623
624			}
625
626
627			if (zoomInButton) {
628
629				zoomInButton.disabled =
630					zoom >= maxZoom;
631
632			}
633
634
635			window.requestAnimationFrame(
636				function () {
637
638
639					modalViewport.scrollLeft =
640						(contentX * ratio) -
641							anchorX;
642
643
644					modalViewport.scrollTop =
645						(contentY * ratio) -
646							anchorY;
647
648				}
649			);
650
651		}
652
653
654
655		/*
656		 * 100%
657		 */
658		function resetZoom() {
659
660
661			if (!baseWidth) {
662				return;
663			}
664
665
666			zoom = 1;
667
668
669			modalImage.style.width =
670				baseWidth +
671					'px';
672
673
674			if (zoomValue) {
675
676				zoomValue.textContent =
677					'100%';
678
679			}
680
681
682			if (zoomOutButton) {
683
684				zoomOutButton.disabled =
685					false;
686
687			}
688
689
690			if (zoomInButton) {
691
692				zoomInButton.disabled =
693					false;
694
695			}
696
697
698			window.requestAnimationFrame(
699				function () {
700
701
702					modalViewport.scrollTop =
703						0;
704
705
706					modalViewport.scrollLeft =
707						Math.max(
708							0,
709							(
710								modalViewport
711									.scrollWidth -
712								modalViewport
713									.clientWidth
714							) /
715							2
716						);
717
718				}
719			);
720
721		}
722
723
724
725		/*
726		 * =====================================================
727		 * モーダルを開く
728		 * =====================================================
729		 */
730
731
732		function openModal(
733			sourceImage,
734			sourceButton
735		) {
736
737
738			lastFocusedElement =
739				sourceButton;
740
741
742			modal.classList.add(
743				'is-open'
744			);
745
746
747			modal.setAttribute(
748				'aria-hidden',
749				'false'
750			);
751
752
753			document.body
754				.classList
755				.add(
756					'is-program-modal-open'
757				);
758
759
760			modalImage.alt =
761				sourceImage.alt ||
762					'';
763
764
765			let initialized =
766				false;
767
768
769
770			function initializeImage() {
771
772
773				if (initialized) {
774					return;
775				}
776
777
778				initialized =
779					true;
780
781
782				const availableWidth =
783					Math.max(
784						100,
785						modalViewport
786							.clientWidth -
787							40
788					);
789
790
791				baseWidth =
792					Math.min(
793						modalImage
794							.naturalWidth ||
795							availableWidth,
796
797						availableWidth
798					);
799
800
801				if (baseWidth <= 0) {
802
803					baseWidth =
804						availableWidth;
805
806				}
807
808
809				resetZoom();
810
811
812				if (modalClose) {
813
814					modalClose.focus();
815
816				}
817
818			}
819
820
821
822			modalImage.onload =
823				initializeImage;
824
825
826			modalImage.src =
827				sourceImage.currentSrc ||
828					sourceImage.src;
829
830
831			if (
832				modalImage.complete &&
833				modalImage.naturalWidth
834			) {
835
836				initializeImage();
837
838			}
839
840		}
841
842
843
844		items.forEach(
845			function (item) {
846
847
848				const button =
849					item.querySelector(
850						'.program__imageButton'
851					);
852
853
854				const image =
855					item.querySelector(
856						'.program__image'
857					);
858
859
860				if (
861					!button ||
862					!image
863				) {
864					return;
865				}
866
867
868				button.addEventListener(
869					'click',
870					function () {
871
872
873						openModal(
874							image,
875							button
876						);
877
878					}
879				);
880
881			}
882		);
883
884
885
886		/*
887		 * =====================================================
888		 * + - 100%
889		 * =====================================================
890		 */
891
892
893		if (zoomInButton) {
894
895
896			zoomInButton.addEventListener(
897				'click',
898				function () {
899
900
901					setZoom(
902						zoom +
903							zoomStep
904					);
905
906				}
907			);
908
909		}
910
911
912
913		if (zoomOutButton) {
914
915
916			zoomOutButton.addEventListener(
917				'click',
918				function () {
919
920
921					setZoom(
922						zoom -
923							zoomStep
924					);
925
926				}
927			);
928
929		}
930
931
932
933		if (resetButton) {
934
935
936			resetButton.addEventListener(
937				'click',
938				resetZoom
939			);
940
941		}
942
943
944
945		/*
946		 * =====================================================
947		 * Pointer Events
948		 *
949		 * PC
950		 * → マウスドラッグ
951		 *
952		 * スマホ
953		 * → 1本指ドラッグ
954		 *
955		 * → 2本指ピンチ
956		 * =====================================================
957		 */
958
959
960		const activePointers =
961			new Map();
962
963
964		let isPanning =
965			false;
966
967
968		let panStartX =
969			0;
970
971
972		let panStartY =
973			0;
974
975
976		let panStartScrollLeft =
977			0;
978
979
980		let panStartScrollTop =
981			0;
982
983
984		let lastPinchDistance =
985			0;
986
987
988
989		function getPointerDistance() {
990
991
992			const points =
993				Array.from(
994					activePointers.values()
995				);
996
997
998			if (points.length < 2) {
999
1000				return 0;
1001
1002			}
1003
1004
1005			const dx =
1006				points[0].x -
1007					points[1].x;
1008
1009
1010			const dy =
1011				points[0].y -
1012					points[1].y;
1013
1014
1015			return Math.hypot(
1016				dx,
1017				dy
1018			);
1019
1020		}
1021
1022
1023
1024		function getPointerMidpoint() {
1025
1026
1027			const points =
1028				Array.from(
1029					activePointers.values()
1030				);
1031
1032
1033			const rect =
1034				modalViewport
1035					.getBoundingClientRect();
1036
1037
1038			if (points.length < 2) {
1039
1040
1041				return {
1042
1043					x:
1044						modalViewport
1045							.clientWidth /
1046							2,
1047
1048					y:
1049						modalViewport
1050							.clientHeight /
1051							2
1052
1053				};
1054
1055			}
1056
1057
1058			return {
1059
1060
1061				x:
1062					(
1063						points[0].x +
1064						points[1].x
1065					) /
1066					2 -
1067					rect.left,
1068
1069
1070				y:
1071					(
1072						points[0].y +
1073						points[1].y
1074					) /
1075					2 -
1076					rect.top
1077
1078			};
1079
1080		}
1081
1082
1083
1084		modalViewport.addEventListener(
1085			'pointerdown',
1086			function (event) {
1087
1088
1089				if (
1090					event.pointerType ===
1091						'mouse' &&
1092					event.button !== 0
1093				) {
1094					return;
1095				}
1096
1097
1098				activePointers.set(
1099					event.pointerId,
1100					{
1101						x:
1102							event.clientX,
1103
1104						y:
1105							event.clientY
1106					}
1107				);
1108
1109
1110				try {
1111
1112					modalViewport
1113						.setPointerCapture(
1114							event.pointerId
1115						);
1116
1117				} catch (error) {
1118
1119					/*
1120					 * Pointer captureが
1121					 * 利用できない場合は何もしない
1122					 */
1123
1124				}
1125
1126
1127
1128				/*
1129				 * 1本指 / マウス
1130				 */
1131				if (
1132					activePointers.size ===
1133						1
1134				) {
1135
1136
1137					isPanning =
1138						true;
1139
1140
1141					panStartX =
1142						event.clientX;
1143
1144
1145					panStartY =
1146						event.clientY;
1147
1148
1149					panStartScrollLeft =
1150						modalViewport
1151							.scrollLeft;
1152
1153
1154					panStartScrollTop =
1155						modalViewport
1156							.scrollTop;
1157
1158
1159					modalViewport
1160						.classList
1161						.add(
1162							'is-dragging'
1163						);
1164
1165				}
1166
1167
1168
1169				/*
1170				 * 2本指
1171				 */
1172				if (
1173					activePointers.size ===
1174						2
1175				) {
1176
1177
1178					isPanning =
1179						false;
1180
1181
1182					lastPinchDistance =
1183						getPointerDistance();
1184
1185				}
1186
1187			}
1188		);
1189
1190
1191
1192		modalViewport.addEventListener(
1193			'pointermove',
1194			function (event) {
1195
1196
1197				if (
1198					!activePointers.has(
1199						event.pointerId
1200					)
1201				) {
1202					return;
1203				}
1204
1205
1206				event.preventDefault();
1207
1208
1209				activePointers.set(
1210					event.pointerId,
1211					{
1212						x:
1213							event.clientX,
1214
1215						y:
1216							event.clientY
1217					}
1218				);
1219
1220
1221
1222				/*
1223				 * 2本指
1224				 * → ピンチ
1225				 */
1226				if (
1227					activePointers.size >=
1228						2
1229				) {
1230
1231
1232					const distance =
1233						getPointerDistance();
1234
1235
1236					if (
1237						lastPinchDistance >
1238							0 &&
1239						distance > 0
1240					) {
1241
1242
1243						const ratio =
1244							distance /
1245								lastPinchDistance;
1246
1247
1248						const midpoint =
1249							getPointerMidpoint();
1250
1251
1252						setZoom(
1253							zoom *
1254								ratio,
1255
1256							midpoint.x,
1257							midpoint.y
1258						);
1259
1260					}
1261
1262
1263					lastPinchDistance =
1264						distance;
1265
1266
1267					return;
1268
1269				}
1270
1271
1272
1273				/*
1274				 * 1本指 / マウス
1275				 * → パン
1276				 */
1277				if (
1278					activePointers.size ===
1279						1 &&
1280					isPanning
1281				) {
1282
1283
1284					const moveX =
1285						event.clientX -
1286							panStartX;
1287
1288
1289					const moveY =
1290						event.clientY -
1291							panStartY;
1292
1293
1294					modalViewport.scrollLeft =
1295						panStartScrollLeft -
1296							moveX;
1297
1298
1299					modalViewport.scrollTop =
1300						panStartScrollTop -
1301							moveY;
1302
1303				}
1304
1305			}
1306		);
1307
1308
1309
1310		function endPointer(event) {
1311
1312
1313			activePointers.delete(
1314				event.pointerId
1315			);
1316
1317
1318
1319			/*
1320			 * ピンチ後に1本残った場合
1321			 * その指でそのまま移動可能にする
1322			 */
1323			if (
1324				activePointers.size ===
1325					1
1326			) {
1327
1328
1329				const remaining =
1330					Array.from(
1331						activePointers.values()
1332					)[0];
1333
1334
1335				isPanning =
1336					true;
1337
1338
1339				panStartX =
1340					remaining.x;
1341
1342
1343				panStartY =
1344					remaining.y;
1345
1346
1347				panStartScrollLeft =
1348					modalViewport
1349						.scrollLeft;
1350
1351
1352				panStartScrollTop =
1353					modalViewport
1354						.scrollTop;
1355
1356
1357				lastPinchDistance =
1358					0;
1359
1360
1361				return;
1362
1363			}
1364
1365
1366
1367			if (
1368				activePointers.size ===
1369					0
1370			) {
1371
1372
1373				isPanning =
1374					false;
1375
1376
1377				lastPinchDistance =
1378					0;
1379
1380
1381				modalViewport
1382					.classList
1383					.remove(
1384						'is-dragging'
1385					);
1386
1387			}
1388
1389		}
1390
1391
1392
1393		modalViewport.addEventListener(
1394			'pointerup',
1395			endPointer
1396		);
1397
1398
1399		modalViewport.addEventListener(
1400			'pointercancel',
1401			endPointer
1402		);
1403
1404
1405
1406		/*
1407		 * =====================================================
1408		 * PC
1409		 * Ctrl + ホイール
1410		 * =====================================================
1411		 */
1412
1413
1414		modalViewport.addEventListener(
1415			'wheel',
1416			function (event) {
1417
1418
1419				if (!event.ctrlKey) {
1420					return;
1421				}
1422
1423
1424				event.preventDefault();
1425
1426
1427				const rect =
1428					modalViewport
1429						.getBoundingClientRect();
1430
1431
1432				const anchorX =
1433					event.clientX -
1434						rect.left;
1435
1436
1437				const anchorY =
1438					event.clientY -
1439						rect.top;
1440
1441
1442				setZoom(
1443
1444					zoom +
1445						(
1446							event.deltaY < 0
1447								? zoomStep
1448								: -zoomStep
1449						),
1450
1451					anchorX,
1452					anchorY
1453
1454				);
1455
1456			},
1457			{
1458				passive: false
1459			}
1460		);
1461
1462
1463
1464		/*
1465		 * =====================================================
1466		 * 閉じる
1467		 * =====================================================
1468		 */
1469
1470
1471		function closeModal() {
1472
1473
1474			activePointers.clear();
1475
1476
1477			isPanning =
1478				false;
1479
1480
1481			lastPinchDistance =
1482				0;
1483
1484
1485			modalViewport
1486				.classList
1487				.remove(
1488					'is-dragging'
1489				);
1490
1491
1492			modal.classList.remove(
1493				'is-open'
1494			);
1495
1496
1497			modal.setAttribute(
1498				'aria-hidden',
1499				'true'
1500			);
1501
1502
1503			document.body
1504				.classList
1505				.remove(
1506					'is-program-modal-open'
1507				);
1508
1509
1510			modalImage.onload =
1511				null;
1512
1513
1514			modalImage.removeAttribute(
1515				'src'
1516			);
1517
1518
1519			modalImage.style.width =
1520				'';
1521
1522
1523			modalImage.alt =
1524				'';
1525
1526
1527			baseWidth =
1528				0;
1529
1530
1531			zoom =
1532				1;
1533
1534
1535			if (
1536				lastFocusedElement &&
1537				typeof
1538					lastFocusedElement.focus ===
1539					'function'
1540			) {
1541
1542				lastFocusedElement.focus();
1543
1544			}
1545
1546		}
1547
1548
1549
1550		if (modalBg) {
1551
1552
1553			modalBg.addEventListener(
1554				'click',
1555				closeModal
1556			);
1557
1558		}
1559
1560
1561
1562		if (modalClose) {
1563
1564
1565			modalClose.addEventListener(
1566				'click',
1567				closeModal
1568			);
1569
1570		}
1571
1572
1573
1574		document.addEventListener(
1575			'keydown',
1576			function (event) {
1577
1578
1579				if (
1580					event.key ===
1581						'Escape' &&
1582					modal.classList
1583						.contains(
1584							'is-open'
1585						)
1586				) {
1587
1588					closeModal();
1589
1590				}
1591
1592			}
1593		);
1594
1595
1596
1597		/*
1598		 * =====================================================
1599		 * リサイズ
1600		 * =====================================================
1601		 */
1602
1603
1604		let resizeTimer =
1605			null;
1606
1607
1608		window.addEventListener(
1609			'resize',
1610			function () {
1611
1612
1613				window.clearTimeout(
1614					resizeTimer
1615				);
1616
1617
1618				resizeTimer =
1619					window.setTimeout(
1620						updateSlider,
1621						100
1622					);
1623
1624			}
1625		);
1626
1627
1628
1629		window.addEventListener(
1630			'load',
1631			updateSlider
1632		);
1633
1634
1635
1636		updateSlider();
1637
1638	}
1639);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.