PageSourceSearch

https://ao-system.net/site/common/js/smoothscroll.js?10

js ao-system.net collected 2026-10-02 03:36:34 UTC 9,586 bytes, 277 lines download raw bytes

1/**
2 * スムーススクロール インラインSVG内アンカータグ対応
3 *
4 * usage:
5 * <script src="smoothscroll.js"></script> //このファイル
6 * 以下、必要に応じて:
7 * <script> smoothScroll.setOption({offset:-200}); </script> //オフセット位置設定(スクロール位置をずらす)
8 * <script> smoothScroll.setOption({speed:20}); </script> //速度設定
9 * <script> smoothScroll.reInit(); </script> //動的に要素を追加したときに再スキャン
10 * <script> smoothScroll.addRender(elm); </script> //aタグを動的に追加した場合にイベントリスナー追加
11 * <script> smoothScroll.elementScroll(elm); </script> //エレメントへスクロール
12 * <script> smoothScroll.anchorScroll('anchorName'); </script> //ページ読み込み後にアンカー名へスクロール
13 * <script> smoothScroll.nameScroll('anchorName'); </script> //アンカー名へスクロール
14 * <script> smoothScroll.idScroll('idName'); </script> //id名へスクロール
15 * <script> smoothScroll.positionScroll(800); </script> //数値指定でスクロール
16 * <script> smoothScroll.bodyScroll(); </script> //bodyへスクロール
17 *
18 * @author ao-system, Inc.
19 *
20 * @date 2015-04-26
21 * @date 2017-07-25
22 * @date 2017-09-22
23 * @date 2017-10-03
24 * @date 2018-06-20
25 * @date 2018-09-11
26 * @date 2019-07-18
27 * @date 2021-01-19
28 * @date 2022-01-06
29 * @date 2024-03-04 classで書き直し。全ての機能を試したわけではない。idへスクロール
30 * @date 2025-06-20 ガタガタ震える場合が有ったので修正
31 * @date 2025-07-18 iPhoneでのスクロールが速すぎる問題を修正。requestAnimationFrameを使用し、スクロール距離に応じた時間ベースのアニメーションに変更。
32 */
33
34class SmoothScroll {
35	#offset =  0;
36	#speed = 15; // Lower value = faster scroll.
37	#minDuration = 400; // ms
38	#maxDuration = 1500; // ms
39
40	#animationFrame =  null;
41
42	// For time-based animation
43	#startTime = null;
44	#startPos = 0;
45	#targetPos = 0;
46	#duration = 0;
47
48	#handleClickHandler = this.#handleClick.bind(this);
49	#preventDefaultHandler = this.#preventDefault.bind(this);
50
51	constructor() {
52		this.#init();
53	}
54
55	//初期設定
56	#init(){
57		window.addEventListener('load',() => {this.#attachAnchorListeners();});
58	}
59
60	/**
61	 * 要素のページ上部からの正確な位置を計算します。
62	 * offsetParentを再帰的にたどり、すべてのオフセットを合計します。
63	 * @param {HTMLElement} elm - 位置を計算する要素。
64	 * @returns {number} ページ上部からのピクセル単位のオフセット。
65	 */
66	#offsettop(elm) {
67		let offsettop = elm.offsetTop;
68		if (elm.offsetParent) {
69			let parent = elm.offsetParent;
70			while (parent) {
71				offsettop += parent.offsetTop;
72				parent = parent.offsetParent;
73			}
74		}
75		return Math.floor(Math.max(0, offsettop + this.#offset));
76	}
77
78	//スクロール位置を返す
79	#scrolltop() {
80		return parseInt(window.scrollY ?? window.pageYOffset ?? document.documentElement.scrollTop) || 0;
81	}
82
83	//イベントをキャンセルする
84	#killEvt(event) {
85	    event.preventDefault();
86	    event.stopPropagation();
87	}
88
89	//デフォルトのホイール動作を無効にする関数
90	#preventDefault(event) {
91		event.preventDefault();
92	}
93
94	//ホイール操作を無効化
95	#disableWheel() {
96		window.addEventListener('wheel', this.#preventDefaultHandler, { passive: false });
97	}
98
99	//ホイール操作を再度有効にする
100	#enableWheel() {
101		window.removeEventListener('wheel', this.#preventDefaultHandler, { passive: false });
102	}
103
104	/**
105	 * スクロールアニメーションのメインループです。
106	 * requestAnimationFrameを使用して、ブラウザの描画タイミングに合わせて滑らかに実行されます。
107	 * 経過時間とイージング関数に基づき、現在のスクロール位置を計算して更新します。
108	 */
109	#runAnimation() {
110		const elapsedTime = performance.now() - this.#startTime;
111		const progress = Math.min(elapsedTime / this.#duration, 1);
112
113		// easeOutQuint easing function: starts fast, ends slower.
114		const ease = (t) => 1 - Math.pow(1 - t, 5);
115		const easedProgress = ease(progress);
116
117		const newPos = this.#startPos + (this.#targetPos - this.#startPos) * easedProgress;
118		window.scrollTo(0, newPos);
119
120		if (progress < 1) {
121			this.#animationFrame = requestAnimationFrame(() => { this.#runAnimation(); });
122		} else {
123			// Animation finished
124			window.scrollTo(0, this.#targetPos); // Ensure it ends at the exact position
125			this.#enableWheel();
126			this.#animationFrame = null;
127		}
128	}
129
130	/**
131	 * アンカーリンクがクリックされた際のイベントを処理します。
132	 * イベントをキャンセルし、リンクのhref属性からターゲットIDを取得してスクロールを開始します。
133	 * @param {MouseEvent} event - クリックイベントオブジェクト。
134	 */
135	#handleClick(event) {
136		this.#killEvt(event);
137		const anchor = event.target.closest('a');
138		if (!anchor) {
139			return;
140		}
141		let anchorTagName = '';
142		// SVG <a> タグ対応
143		if (typeof anchor.href === 'object' && 'baseVal' in anchor.href) {
144			anchorTagName = anchor.href.baseVal.replace(/^#/, '');
145		} else if (anchor.hash) {
146			anchorTagName = anchor.hash.replace(/^#/, '');
147		} else {
148			return;
149		}
150
151		if (this.#animationFrame) {
152			cancelAnimationFrame(this.#animationFrame);
153			this.#animationFrame = null;
154		}
155
156		const targetElement = document.getElementById(anchorTagName);
157		if (targetElement) {
158			this.elementScroll(targetElement);
159		}
160	}
161
162	/**
163	 * ページ内のすべてのアンカーリンク(`a[href*="#"]`)にクリックイベントリスナーを設定します。
164	 * SVG内のリンクにも対応しています。
165	 */
166	#attachAnchorListeners() {
167		const anchorTags = document.querySelectorAll('a[href]');
168		anchorTags.forEach((anchorTag) => {
169			if (anchorTag.href) {
170				if (anchorTag.href.baseVal) {	//SVGの場合 e.g. <svg><a href="#footer"><rect .. /></a></svg>
171					if (anchorTag.href.baseVal.startsWith('#')) {
172						((elm) => {
173							elm.removeEventListener('click',this.#handleClickHandler);
174							elm.addEventListener('click',this.#handleClickHandler);
175						})(anchorTag);
176					}
177				} else {
178					if (anchorTag.href.indexOf('#') != -1 && ((anchorTag.pathname == location.pathname) || ('/' + anchorTag.pathname == location.pathname))) {
179						((elm) => {
180							elm.removeEventListener('click',this.#handleClickHandler);
181							elm.addEventListener('click',this.#handleClickHandler);
182						})(anchorTag);
183					}
184				}
185			}
186		});
187	}
188
189	/**
190	 * スクロール動作のオプションを設定します。
191	 * @param {object} obj - オプションを含むオブジェクト。 e.g. { offset: -100, speed: 20 }
192	 */
193	setOption(obj) {
194		if (obj.offset !== undefined) {
195			this.#offset = obj.offset;	//スクロール位置のオフセット
196		}
197		if (obj.speed !== undefined) {
198			this.#speed = obj.speed;	//速度設定
199		}
200	}
201
202	//初期設定 再度
203	reInit() {
204		this.#attachAnchorListeners();
205	}
206
207	//イベントリスナー 後から個別に追加
208	addRender(anchorElm) {
209		if (anchorElm.href) {
210			if (anchorElm.href.baseVal) {	//SVGの場合 e.g. <svg><a href="#footer"><rect .. /></a></svg>
211				if (anchorElm.href.baseVal.slice(0,1) == '#') {
212					anchorElm.removeEventListener('click',this.#handleClickHandler);
213					anchorElm.addEventListener('click',this.#handleClickHandler);
214				}
215			} else {
216				if (anchorElm.href.indexOf('#') != -1 && ((anchorElm.pathname == location.pathname) || ('/' + anchorElm.pathname == location.pathname))) {
217					anchorElm.removeEventListener('click',this.#handleClickHandler);
218					anchorElm.addEventListener('click',this.#handleClickHandler);
219				}
220			}
221		}
222	}
223
224	/**
225	 * 指定されたHTML要素へのスクロールを開始します。
226	 * アニメーションの初期設定(開始位置、目標位置、デュレーション)を行い、ループを開始します。
227	 * @param {HTMLElement} elm - スクロール先の要素。
228	 */
229	elementScroll(elm) {
230		if (elm) {
231			this.#disableWheel();
232			this.#startPos = this.#scrolltop();
233			this.#targetPos = this.#offsettop(elm);
234			
235			const distance = Math.abs(this.#targetPos - this.#startPos);
236
237			// Calculate a dynamic duration based on the scroll distance.
238			const calculatedDuration = Math.sqrt(distance) * this.#speed;
239
240			// Clamp the duration to the configured min/max range.
241			this.#duration = Math.max(this.#minDuration, Math.min(calculatedDuration, this.#maxDuration));
242
243			if (this.#animationFrame) {
244				cancelAnimationFrame(this.#animationFrame);
245			}
246
247			this.#startTime = performance.now();
248			this.#animationFrame = requestAnimationFrame(() => { this.#runAnimation(); });
249		}
250	}
251
252	//アンカー名へスクロールする Load時にセット
253	anchorScroll(nameValue) {
254		window.addEventListener('load',() => {
255			if (document.getElementById('loading')) {	//ページロード中表示があればそれを消す
256				document.getElementById('loading').outerHTML = '';
257			}
258			this.elementScroll(document.getElementsByName(nameValue)[0] ?? null);
259		});
260	}
261
262	//name名へスクロールする。ダイレクト実行
263	nameScroll(nameValue) {
264		this.elementScroll(document.getElementsByName(nameValue)[0] ?? null);
265	}
266
267	//ID名へスクロールする。ダイレクト実行
268	idScroll(idValue) {
269		this.elementScroll(document.getElementById(idValue));
270	}
271
272	//bodyへスクロールする。ダイレクト実行
273	bodyScroll() {
274		this.elementScroll(document.body);
275	}
276}
277const smoothScroll = new SmoothScroll();

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.