PageSourceSearch

https://sokki.or.jp/_administrator/js/common.js

js sokki.or.jp collected 2026-10-02 19:13:37 UTC 227,235 bytes, 6,396 lines download raw bytes

1var domain_name = 'sokki\x2eor\x2ejp';
2
3var http_url = 'https://sokki.or.jp';
4var https_url = 'https://sokki.or.jp';
5var php_path = '/_administrator/connector.php';
6var connector = '/_administrator/connector.php';
7var base_url = '';
8var site_path = '';
9var cookie_first_url = 'cms_first_url';
10//フロントエンドAPI用Cookie設定.
11if(location.href.match(/[\?|&]access_token=.+/)){
12	let cookies = document.cookie.split(';');
13	let expire = 24 * 60 * 60;
14	let is_static_cookie = false;
15	for (let ii = 0; ii < cookies.length; ii++) {
16		let cookie = cookies[ii];
17		while (cookie.charAt(0) == ' ') {
18			cookie = cookie.substring(1, cookie.length);
19		}
20		let name = cookie.split('=')[0];
21		let value = cookie.split('=')[1];
22		if(name === 'static' && value === 'false'){
23			is_static_cookie = true;
24			break;
25		}
26	}
27	if(!is_static_cookie){
28		document.cookie = "static=false;max-age="+expire+"; path=/;secure=true;samesite=LAX";
29		location.reload();
30	}
31}
32
33window.addEventListener('load', function(event) {
34	if($('selecter') instanceof jQuery) {
35		jQuery.noConflict();
36	}
37});
38
39// Smart Phone用モバイルサイトへのリンク
40var strUA = navigator.userAgent.toLowerCase();
41var uach = navigator.userAgentData;
42
43Event.observe(window,'load',function(){
44	if( !!document.getElementById('published') )
45	{
46		if( strUA.match(/iphone/) || strUA.match(/ipod/) || strUA.match(/android/) || (uach && uach.mobile))
47		{
48			var div = document.createElement('div');
49			div.id = 'cmsMobileButton';
50			div.style.textAlign = 'center';
51			div.style.height = '50px';
52			var a = document.createElement('a');
53			a.href = 'https://sokki.or.jp/index.php?a=free_page/goto_mobile&referer='+encodeURI(location.href);
54			a.innerHTML = '表示:モバイル';
55			div.appendChild(a);
56			document.body.appendChild(div);
57			a.style.margin = '30px auto';
58			a.style.padding = '5px 10px';
59			a.style.border = '1px solid #233656';
60			a.style.width = 'auto';
61			a.style.color = '#ffffff';
62			a.style.backgroundColor = '#394d68';
63			a.style.backgroundImage = '-webkit-gradient(linear, left top, left bottom, from(#5e789f), to(#42566f))';
64			a.style.fontSize = '12px';
65			a.style.borderRadius = '4px';
66			a.style.WebkitBorderRadius = '4px';
67			a.style.textShadow = '#000000 0 -1px 1px';
68			a.style.textDecoration = 'none';
69		}
70	}
71});
72var mobile = false;
73if( strUA.match(/iphone/) || strUA.match(/ipod/)  || strUA.match(/ipad/) || strUA.match(/android/) || (uach && uach.mobile))
74{
75	mobile = true;
76	function pointerX(event) {
77		var docElement = document.documentElement;
78		body = document.documentElement || document.body || { scrollLeft: 0 };
79
80		if (event.changedTouches) return (event.changedTouches[0].clientX +
81		(docElement.scrollLeft || body.scrollLeft) -
82		(docElement.clientLeft || 0));
83
84		return event.pageX || (event.clientX +
85		(docElement.scrollLeft || body.scrollLeft) -
86		(docElement.clientLeft || 0));
87	}
88
89	function pointerY(event) {
90		var docElement = document.documentElement;
91		body = document.documentElement || document.body || { scrollTop: 0 };
92
93		if (event.changedTouches) return (event.changedTouches[0].clientY +
94		(docElement.scrollLeft || body.scrollLeft) -
95		(docElement.clientLeft || 0));
96
97		return  event.pageY || (event.clientY +
98		(docElement.scrollTop || body.scrollTop) -
99		(docElement.clientTop || 0));
100	}
101}
102// do_action
103function do_action (act,title) {
104	self.location.href='?q='+act;
105	if(title != undefined ){
106		var win = parent.Windows.getFocusedWindow();
107		win.setTitle(title);
108	}
109}
110
111/*
112 * Ajax Request ( Return Scripts )
113 *
114 *--------------------------------------------------------- */
115
116/**
117 * ajax_eval ajaxでconnectorと通信し、レスポンスをeval実行する。
118 *
119 * ajax_eval( string data, boolean async )
120 *
121 * @param string アクション/メソッドのパラメータ(ex. site_manager/add_site )
122 * @param boolean 非同期フラグ true: 非同期通信 false: 同期通信
123 *
124 * @return void
125 */
126function ajax_eval( data, async )
127{
128	if( async == null ) async = true;
129	if( !data.match(/&q=/) ) data = '&q='+data;
130	new Ajax.Request(connector, {
131		method: 'post',
132		postBody : data,
133		asynchronous  : async,
134		onSuccess : function(obj){
135			if(obj.responseText){
136				eval(obj.responseText);
137			}
138		},
139		onFailure: function(obj){
140			alert('エラーが発生しました。');
141		}
142	});
143}
144// function ajax_eval_debug( data, async )
145// {
146// alert(connector);
147// alert(data);
148// 	if( async == null ) async = true;
149// 	if( !data.match(/&q=/) ) data = '&q='+data;
150// 	new Ajax.Request(connector, {
151// 		method: 'post',
152// 		postBody : data,
153// 		asynchronous  : async,
154// 		onSuccess : function(obj){
155// alert(obj.responseText);
156// 			if(obj.responseText){
157// alert(obj.responseText);
158// 			eval(obj.responseText);
159// 			}
160// 		}
161// 	});
162// }
163/**
164 * ajax_json_eval ajaxでconnectorと通信し、レスポンスをJSON形式で取得しeval実行する。
165 *
166 * ajax_json_eval( string data )
167 *
168 * @param string アクション/メソッドのパラメータ(ex. site_manager/add_site )
169 *
170 * @return void
171 */
172function ajax_json_eval( data ) {
173	if( !data.match(/&q=/) ) data = '&q='+data;
174	new Ajax.Request(connector, {
175		method: 'post',
176		postBody : data,
177		onSuccess : function(obj){
178			if(obj.responseText){
179				eval('ret='+obj.responseText);
180				if( ret.type == 'script' ) {
181					eval(ret.data);
182				} else {
183//					alert(obj.responseText);
184				}
185			}
186		},
187		onFailure: function(obj){
188			alert('エラーが発生しました。');
189		}
190	});
191}
192
193/**
194 * ajax_callback ajaxでサーバーと通信し、レスポンスをコールバック関数に引渡しする。
195 *
196 * ajax_callback( string data, string func, boolean async )
197 *
198 * @param string アクション/メソッドのパラメータ(ex. site_manager/add_site )
199 * @param string 関数名
200 * @param boolean 非同期フラグ true: 非同期通信 false: 同期通信
201 *
202 * @return void
203 */
204function ajax_callback( data, func, async ) {
205	if( async == null ) async = true;
206	if( !data.match(/&q=/) ) data = '&q='+data;
207	new Ajax.Request(connector, {
208		method: 'post',
209		postBody : data,
210		asynchronous  : async,
211		onSuccess : eval(func),
212		onFailure: function(obj){
213			alert('エラーが発生しました。');
214		}
215	});
216}
217
218/**
219 * ajax_callback_form_upload
220 * 汎用Ajaxファイルアップロード関数(FormData + XMLHttpRequest版)
221 *
222 * @param {string} query - コントローラとアクションのパス(例: 'host_manager/upload_backup/1')
223 * @param {HTMLFormElement} form - <form> 要素
224 * @param {Object} options - オプション: { onSuccess: func(result, form), onError: func(result, form) }
225 */
226function ajax_callback_form_upload(query, form, options = {}) {
227	const url = connector + '?q=' + encodeURIComponent(query);
228	const formData = new FormData(form);
229	const xhr = new XMLHttpRequest();
230
231	xhr.open('POST', url, true);
232
233	xhr.onload = function () {
234		if (xhr.status >= 200 && xhr.status < 300) {
235			let result;
236			try {
237				result = JSON.parse(xhr.responseText);
238			} catch (e) {
239				alert('レスポンスの解析に失敗しました。');
240				return;
241			}
242			if (result.success) {
243				if (typeof options.onSuccess === 'function') {
244					options.onSuccess(result, form);
245				}
246			} else {
247				if (typeof options.onError === 'function') {
248					options.onError(result, form);
249				}
250			}
251		} else {
252			if (typeof options.onError === 'function') {
253				options.onError({ success: false, err_message: 'HTTPエラー: ' + xhr.status }, form);
254			} else {
255				alert('HTTPエラーが発生しました。');
256			}
257		}
258	};
259
260	xhr.onerror = function () {
261		if (typeof options.onError === 'function') {
262			options.onError({ success: false, err_message: '通信エラーが発生しました。' }, form);
263		} else {
264			alert('通信エラーが発生しました。');
265		}
266	};
267
268	xhr.send(formData);
269}
270
271
272/**
273 * ajax_update ajaxでサーバーと通信し、レスポンスを指定したDOMエレメントに挿入する。
274 *
275 * ajax_update( string data, string elm )
276 *
277 * @param string アクション/メソッドのパラメータ(ex. site_manager/add_site )
278 * @param object innerHTMLを置換するエレメントのID属性値
279 *
280 * @return void
281 */
282function ajax_update( data, elm ){
283	if( !data.match(/&q=/) ) data = '&q='+data;
284	new Ajax.Updater(
285		{success:elm},
286		connector, {
287			method: "post",
288			postBody : data,
289			evalScripts: true,
290			onFailure:function(httpObj){
291				alert('エラーが発生しました。');
292				$(elm).innerHTML = "Error!!";
293			}
294		}
295	);
296}
297
298/**
299 * ajax_eval ajaxでconnectorと通信し、レスポンスをeval実行する。公開サイト用。。。
300 *
301 * ajax_eval( string data, boolean async )
302 *
303 * @param string アクション/メソッドのパラメータ(ex. site_manager/add_site )
304 * @param boolean 非同期フラグ true: 非同期通信 false: 同期通信
305 *
306 * @return void
307 */
308function ajax_eval_p( data, async )
309{
310	if( async == null ) async = true;
311	if( !data.match(/&a=/) ) { data = '&a='+data; }
312	new Ajax.Request('/index.php',
313	{
314		method: 'get',
315		 parameters : data,
316		 asynchronous  : async,
317		 onSuccess: function(obj) {
318			if(obj.responseText){
319//			alert(obj.responseText);
320			eval(obj.responseText);
321			}
322		},
323		onFailure: function(obj){
324			alert('エラーが発生しました。');
325		}
326	});
327}
328/**
329 * ajax_update_p ajaxでサーバーと通信し、レスポンスを指定したDOMエレメントに挿入する。公開サイト用。。。
330 *
331 * ajax_update_p( string data, string elm )
332 *
333 * @param string アクション/メソッドのパラメータ(ex. site_manager/add_site )
334 * @param object innerHTMLを置換するエレメントのID属性値
335 *
336 * @return void
337 */
338function ajax_update_p( data, elm )
339{
340	if( !data.match(/&q=/) ) data = '&a='+data;
341	new Ajax.Updater(
342		{success:elm},
343		'/index.php', {
344			method: "get",
345			parameters : data,
346			evalScripts: true,
347			onFailure:function(httpObj){
348				$(elm).innerHTML = "Error!!";
349			},
350			onFailure: function(obj){
351				alert('エラーが発生しました。');
352			}
353		}
354	);
355}
356
357function ajax_submit_p(form_obj){
358    post_data = Form.serialize(form_obj);
359	new Ajax.Request('/index.php', {
360		method: 'post',
361		postBody : post_data,
362		onSuccess : function(obj){
363			if(obj.responseText){
364				eval(obj.responseText);
365			}
366		},
367		onFailure: function(obj){
368			alert('エラーが発生しました。');
369		}
370	});
371}
372
373/**
374 * ajax_dialog dataで指定したパラメータのレスポンスをダイアログで開く
375 *
376 * ajax_dialog( string data, integer x, integer y )
377 *
378 * @param string アクション/メソッドのパラメータ(ex. site_manager/add_site )
379 * @param integer 開くダイアログの幅(px) デフォルト480px
380 * @param integer 開くダイアログの高さ(px) デフォルト320px
381 *
382 * @return void
383 */
384var mydialog;
385function trigger_outer_dialog_button_overlay(){
386	var adminDialog = this.frameElement.closest('#admin_dialog');
387	var outerDialogRowMid = adminDialog.querySelectorAll('#cms_dialog_rowmid')[0];
388	var outerDialogButtonsOverlay = adminDialog.querySelectorAll('#hideButtons')[0];
389
390	if (!!outerDialogButtonsOverlay) {
391		outerDialogRowMid.removeChild(outerDialogButtonsOverlay);
392	} else {
393		var span = document.createElement('span');
394		span.id = 'hideButtons';
395		span.style.display = 'inline-block';
396		span.style.height = '50px';
397		span.style.width = '1001px';
398		span.style.background = '#fff';
399		span.style.position = 'absolute';
400		span.style.bottom ='-50px';
401		span.style.left ='10px';
402		span.style.zindex =10500;
403		outerDialogRowMid.appendChild(span);
404	}
405}
406function ajax_dialog(data,x,y,cb)
407{
408	if( !data.match(/&q=/) ) data = '&q='+data;
409	if( !x ) x = 480;
410	if( !y ) y = 320;
411	if( typeof cb === "function"){
412		cb();
413	}
414	Dialog.info(
415		{
416			url: connector,
417			options: {
418				method: 'post',
419				postBody : data,
420				asynchronous : false,
421				evalScripts: true,
422				buttonClass: 'cms-btn'
423			}
424		},
425		{
426			className:"alert",
427			width:x,
428			height:y,
429			zIndex:10000,
430			onClose:function(){
431				if( typeof cb === "function"){
432					cb();
433				}
434			}
435		}
436	);
437}
438
439/**
440 * ajax_submit FormをajaxでPOSTし、結果をeval実行する
441 *
442 * ajax_submit( obj form )
443 *
444 * @param obj フォームオブジェクト
445 *            ※フォーム内に<input type="hidden" name="do" value="クラス名/メソッド名" />の記述があること
446 *
447 * @return void
448 */
449function ajax_submit(form_obj){
450    post_data = Form.serialize(form_obj);
451	new Ajax.Request(connector, {
452		method: 'post',
453		postBody : post_data,
454		onSuccess : function(obj){
455			if(obj.responseText){
456				eval(obj.responseText);
457			}
458		},
459		onFailure: function(obj){
460			alert('エラーが発生しました。');
461		}
462	});
463}
464
465/**
466 * ajax_submit FormをajaxでPOSTし、レスポンスを指定したDOMエレメントに挿入する。
467 *
468 * ajax_submit( string data, obj form, string elment)
469 *
470 * @param obj フォームオブジェクト
471 *            ※フォーム内に<input type="hidden" name="do" value="クラス名/メソッド名" />の記述があること
472 *
473 * @return void
474 */
475function ajax_submit_update( data, formelm, elm ) {
476	if( !!$('easy_html') && !!oEditor ) {
477		$('easy_html').value = oEditor.GetXHTML();
478		oEditor=null;
479	} else if ( !!$('cmp_html') && !!oEditor ) {
480		$('cmp_html').value = oEditor.GetXHTML();
481		oEditor=null;
482	}
483	if( !data.match(/&q=/) ) data = '&q='+data+'&'+Form.serialize(formelm);
484	new Ajax.Updater(
485		{success:elm},
486		connector, {
487			method: "post",
488			postBody : data,
489			evalScripts: true,
490			onFailure:function(httpObj){
491				$(elm).innerHTML = "Error!!";
492			}
493		}
494	);
495	return false;
496}
497
498/**
499 * alert_dialog アラートダイアログを表示する
500 *
501 * alert_dialog( string message[, integer height[, string function]] )
502 *
503 * @param string ダイアログ内に表示するメッセージ(HTML可)
504 * @param integer ダイアログの高さ(省略時100px)
505 * @param string OKボタン押下時に実行する関数名もしくは匿名関数(function(){...})
506 *
507 * @return void
508 */
509function alert_dialog(msg, h, func) {
510	if( !h ) h = 150;
511	if(jQuery('#alert').length > 0) jQuery('#alert').attr('id','alert-bf');
512	Dialog.alert(msg, {
513	windowParameters: {width:480, height:h, zIndex:1000000},
514	className:"alert",
515	id:"alert",
516	okLabel: "OK",
517	ok: func,
518	buttonClass: 'cms-btn'
519	});
520	setTimeout( function(){
521		jQuery('#alert').find('.ok_button').focus();
522	},100);
523}
524/**
525 * alert_dialog_reload アラートダイアログを表示し、OKボタン押下で画面をリロードする
526 *
527 * alert_dialog_reload( string message[, integer height] )
528 *
529 * @param string ダイアログ内に表示するメッセージ(HTML可)
530 * @param integer ダイアログの高さ(省略時100px)
531 *
532 * @return void
533 */
534function alert_dialog_reload(msg, h) {
535	if( !h ) h = 150;
536	if(jQuery('#alert').length > 0) jQuery('#alert').attr('id','alert-bf');
537	Dialog.alert(msg, {
538	windowParameters: {width:480, height:h, zIndex:1000000},
539	className:"alert",
540	id:"alert",
541	okLabel: "OK",
542	buttonClass: 'cms-btn',
543	ok: function(){ location.reload(); }
544	});
545}
546/**
547 * confirm_dialog 確認ダイアログを表示する
548 *
549 * confirm_dialog( string message[, integer height[, string function]] )
550 *
551 * @param string ダイアログ内に表示するメッセージ(HTML可)
552 * @param integer ダイアログの高さ(省略時100px)
553 * @param string OKボタン押下時に実行する関数名もしくは匿名関数(function(){...})
554 *
555 * @return void
556 */
557function confirm_dialog(msg, func, h) {
558	if( !h ) h = 150;
559	if(jQuery('#alert').length > 0) jQuery('#alert').attr('id','alert-bf');
560	Dialog.confirm(msg, {
561	windowParameters: {width:480, height:h, zIndex:1000000, duration:0.1},
562	className:"alert",
563	id:"alert",
564	okLabel: "OK",
565	ok:func,
566	cancelLabel: "キャンセル",
567	buttonClass: "cms-btn",
568	cancel:function(){Dialog.closeInfo();}
569	});
570}
571/**
572 * confirm_dialog_update 確認ダイアログを表示し、OKボタン押下でサーバーとajax通信しレスポンスを指定したDOMエレメントに挿入する。
573 *
574 * confirm_dialog_update( string message, string data, string element[, integer height] )
575 *
576 * @param string ダイアログ内に表示するメッセージ(HTML可)
577 * @param string アクション/メソッドのパラメータ(ex. site_manager/add_site )
578 * @param object innerHTMLを置換するエレメントのID属性値
579 * @param integer ダイアログの高さ(省略時100px)
580 *
581 * @return void
582 */
583function confirm_dialog_update(msg, data, elm, h) {
584	if( !h ) h = 150;
585	if(jQuery('#alert').length > 0) jQuery('#alert').attr('id','alert-bf');
586	Dialog.confirm(msg, {
587	windowParameters: {width:320, height:h, zIndex:1000000},
588	className:"alert",
589	id:"alert",
590	okLabel: "OK",
591	cancelLabel: "キャンセル",
592	buttonClass: "cms-btn",
593	ok:function(){ajax_update(data,elm);Dialog.closeInfo();},
594	cancel:function(){Dialog.closeInfo();}
595	});
596
597}
598/**
599 * info_dialog 情報ダイアログを表示する
600 *
601 * info_dialog( string message[, integer width][, integer height][, boolean progress] )
602 *
603 * @param string ダイアログ内に表示するメッセージ(HTML可)
604 * @param integer ダイアログの幅(省略時320px)
605 * @param integer ダイアログの高さ(省略時100px)
606 * @param boolean ダイアログ内にプログレスアイコンを表示するか否か
607 *
608 * @return void
609 */
610function info_dialog(msg, w, h, progress) {
611	if( !w ) w = 320;
612	if( !h ) w = 150;
613	if( progress !== false ) progress = true;
614	if(jQuery('#alert').length > 0) jQuery('#alert').attr('id','alert-bf');
615	Dialog.info(msg, {
616	className:"alert",
617	id:"alert",
618	buttonClass: 'cms-btn',
619	windowParameters: {width:w, height:h, zIndex:1000000},
620	showProgress:progress
621	});
622}
623function sub_dialog(msg,x,y) {
624	if( !x ) x = 320;
625	if( !y ) y = 100;
626	if(jQuery('#alert').length > 0) jQuery('#alert').attr('id','alert-bf');
627	Dialog.info(msg, {
628	className:"alert",
629	id:"alert",
630	buttonClass: 'cms-btn',
631	windowParameters: {width:x, height:y, zIndex:1000000},
632	showProgress:false
633	});
634}
635function sub_dialog_for_ad(msg,x,y) {
636	if( !x ) x = 320;
637	if( !y ) y = 100;
638	if(jQuery('#alert').length > 0) jQuery('#alert').attr('id','alert-bf');
639	Dialog.info(msg, {
640	className:"alert",
641	id:"dialog",
642	buttonClass: 'cms-btn',
643	windowParameters: {width:x, height:y, zIndex:1000000},
644	showProgress:false
645	});
646}
647/**
648 * close_dialog 情報ダイアログのメッセージを変更して1.5秒後に閉じる
649 *
650 * close_dialog( string message )
651 *
652 * @param string ダイアログ内に表示するメッセージ(HTML可)
653 *
654 * @return void
655 */
656function close_dialog(msg) {
657	if(jQuery('#modal_dialog_message').length > 0){
658		Dialog.setInfoMessage(msg);
659	} else {
660		jQuery('.alert_message').html(msg);
661		jQuery('.alert_buttons').html('');
662	}
663	setTimeout("Dialog.closeInfo()", 1500);
664}
665/**
666 * set_message 情報ダイアログのメッセージを変更する
667 *
668 * set_message( string message )
669 *
670 * @param string ダイアログ内に表示するメッセージ(HTML可)
671 *
672 * @return void
673 */
674function set_message(msg,loading){
675	if(jQuery('#modal_dialog_message').length > 0){
676		Dialog.setInfoMessage(msg);
677	} else {
678		jQuery('.alert_message').html(msg);
679		jQuery('.alert_buttons').html('');
680		if(loading){
681			jQuery('.alert_buttons').html("<div id='modal_dialog_progress' class='alert_progress'>  </div>");
682		}
683	}
684}
685/**
686 * result_message 開いているダイアログにメッセージと閉じるボタンをセットする
687 *
688 * result_message( string message )
689 *
690 * @param string ダイアログ内に表示するメッセージ(HTML可)
691 *
692 * @return void
693 */
694function result_message(msg,okscript){
695	if(okscript.length==0) okscript = 'Dialog.closeInfo()';
696	var content = "<div class='alert_message'>" + msg  + "</div>"
697				+ "<div class='alert_buttons'>"
698				+ "<input type='button' value='OK' onclick='" + okscript + "' class='cms-btn'/>"
699				+ "</div>";
700	if(jQuery('#modal_dialog_message').length > 0){
701		Dialog.setInfoMessage(content);
702	} else {
703		jQuery('#alert_content').html(content);
704	}
705}
706/**
707 * set_size_dialog ダイアログのサイズを変更する
708 *
709 * set_size_dialog( string element_id )
710 *
711 * @param string エレメントID、もしくはオブジェクト
712 *
713 * @return void
714 */
715function set_size_dialog(w,h)
716{
717	var winobj = self.Windows.getFocusedWindow();
718	if( winobj === null ) return;
719	winobj.setSize(w,h);
720	winobj.showCenter();
721}
722/**
723 * toggle エレメントの表示/非表示を切り替える
724 *
725 * toggle( string element_id )
726 *
727 * @param string エレメントID、もしくはオブジェクト
728 *
729 * @return void
730 */
731function toggle(elm)
732{
733	if( Element.visible(elm) )
734	{
735		$(elm).hide();
736	}
737	else
738	{
739		$(elm).show();
740	}
741}
742
743function set_cookie(keyname, data, expire_flag)
744{
745	theDay   = 1;
746	setDay = new Date();
747	setDay.setTime(setDay.getTime()+(theDay*1000*60*60*24));
748	expDay = setDay.toGMTString();
749	val = get_cookie(keyname);
750	if( val !== '' ) {
751		if(keyname === 'error_report'){
752			val = escapeRegExp(encodeURIComponent(val));
753		}else{
754			val = encodeURIComponent(val);
755		}
756		var re = new RegExp(encodeURIComponent(keyname) + "="+ val +";");
757		document.cookie.replace(re,'');
758	}
759	if( expire_flag === true) {
760		document.cookie = encodeURIComponent(keyname) + "="+encodeURIComponent(data)+"; expires=" + _raw(expDay) + "; path=/";
761	} else {
762		document.cookie = encodeURIComponent(keyname) + "="+encodeURIComponent(data)+ "; path=/";
763	}
764}
765
766function escapeRegExp(string) {
767  // 特殊文字をエスケープ
768  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
769}
770
771function get_cookie(keyname)
772{
773	theData   = "";
774	theName   = encodeURIComponent(keyname)+"=";
775	theCookie = document.cookie+";";
776	start = theCookie.indexOf(theName);
777	if (start != -1)
778	{
779		end = theCookie.indexOf(";",start);
780		theData = decodeURIComponent(theCookie.substring(start+theName.length,end));
781	}
782	return theData;
783}
784
785
786function open_dialog(url, x, y, title, status, modal, dialog_name, reload )
787{
788	if( typeof context_menu == 'object' ) context_menu.oncontextmenu_hide(); 
789	if( !url ) return false;
790	if( !x ) x = '1000';
791	if( !y ) y = '640';
792	var scr = screenInfomation();
793	y = scr.nowHeight - 30;
794	if( x.toString().match(/([0-9]+)%/) ) {
795		ratio = RegExp.$1 / 100;
796		x = Math.floor(get_brouser_width() * ratio);
797	}
798	if( x > get_brouser_width() ) {
799		x = Math.floor(get_brouser_width() * 0.95);
800	}
801	// y = eval(y)+30;
802	if( !title ) title = '';
803	if( !status ) status = '';
804	if( !modal ) modal = true;
805	if( !dialog_name ) dialog_name = 'admin_dialog';
806	if( !reload ) reload = false;
807	jQuery('#overlay_modal').remove();
808	jQuery('#admin_dialog').remove();
809	var win = new Window(dialog_name,{
810		url:url,
811		className: 'dialog',
812		destroyOnClose: true,
813		width:x,
814		height:y,
815		zIndex: 100005,
816		resizable: true,
817		minimizable: false,
818		maximizable: false,
819		title: title,
820		recenterAuto: true,
821		/*hideEffect: Effect.SwitchOff,*/
822		// parent: document.getElementById('cms_contents_area'),
823		draggable:true,
824		wiredDrag: false,
825		showEffectOptions:{duration:0.1},
826		hideEffectOptions:{duration:0},
827		duration:0,
828		onResize:function(){
829			try{
830				var parent = parent;
831				var p = parent.document;
832			}catch(e){
833				var parent = window;
834			}
835			if( !parent.Windows ) return;
836			var winobj = parent.Windows.getFocusedWindow();
837			if( winobj === null ) return;
838			// ダイアログ外側のサイズ
839			var size = winobj.getSize();
840			// スクリーンの現在のサイズ
841			var scr = screenInfomation();
842			// ダイアログのインラインフレーム内コンテンツの高さ
843			var h = 240;
844			if(  document.body && document.body.offsetHeight ) {
845				h = document.body.offsetHeight;
846			} else if ( document.documentElement && document.documentElement.offsetHeight )  {
847				h = document.documentElement.offsetHeight;
848			}
849			// ダイアログのヘッダ+フッタの高さ
850			if( !!parent.$('admin_dialog_row1') && !!parent.$('admin_dialog_row3')  && !!parent.$('cms_dialog_ctrl_td') ) {
851				var hof = parent.Element.getDimensions('admin_dialog_row1').height + parent.Element.getDimensions('admin_dialog_row3').height  + parent.jQuery('#cms_dialog_rowmid').height();
852				//最低限のウインドウの高さのマージン(かたっぽ)
853				var min_h_margin = 15;
854				if( h > scr.nowHeight - hof - min_h_margin*2 || 1 ) h = scr.nowHeight - hof - min_h_margin*2 ;
855				winobj.setSize(size['width'],h);
856				size = winobj.getSize();
857				real_height = size.height + winobj.heightS + winobj.heightN;
858				x = ((scr.width/2) - size.width/2);
859				y = min_h_margin;//(( scr.nowHeight - (real_height+hof) )/2 + scr.y + min_h_margin * 2);// - jQuery(window).scrollTop() ;
860				winobj.showCenter(null,y,x);
861			}
862
863			$('admin_dialog_content').height = $('admin_dialog_table_content').height;
864			var $iframe = jQuery($('admin_dialog_content').contentWindow.document);
865			var frameHeight = jQuery('#admin_dialog_table_content').height();
866			var height = frameHeight-$iframe.find('#cms_navigation_main .cms-navigation-wrap--head').height();
867			$iframe.find('.cms-navigation-wrap__sub-scroll').height(height);
868			var addHeight = 0;
869			if($iframe.find('.cms-navigation-nav__table').length > 0 ){
870				addHeight = $iframe.find('.cms-navigation-nav__table').css('padding').replace('px','') * 2;
871			}
872			if($iframe.find('.cms-navigation-wrap__main').length > 0){
873				addHeight += $iframe.find('.cms-navigation-wrap__main').height();
874			}
875			if($iframe.find('#cms_navigation_main .cms-table-list--scroll').length > 0){
876				addHeight += $iframe.find('#cms_navigation_main .cms-table-list--scroll').height();
877			}
878			var height = frameHeight - addHeight;
879			$iframe.find('#cms_navigation_list').height(height);
880			jQuery($('admin_dialog_content').contentWindow.document).find('#cms_dialog_container').height(jQuery('#admin_dialog_table_content').height());
881			jQuery($('admin_dialog_content').contentWindow.document.html).height($('admin_dialog_table_content').height);
882			jQuery($('admin_dialog_content').contentWindow.document).find('iframe').each(function(){
883				if( this.id && this.parentNode.tagName != 'TD' && jQuery(this).position().top > 0) {
884					jQuery(this).height(jQuery('#admin_dialog_content').height()-jQuery(this).position().top);
885				}
886			});
887		},
888		onMaximize:function(){
889			// $('admin_dialog_content').height = $('admin_dialog_table_content').height;
890			// jQuery($('admin_dialog_content').contentWindow.document).find('#cms_navigation_list').height(jQuery('#admin_dialog_table_content').height()-jQuery($('admin_dialog_content').contentWindow.document).find('#cms_navigation_main>.cms_list_table').height());
891			// jQuery($('admin_dialog_content').contentWindow.document).find('#cms_dialog_container').height(jQuery('#admin_dialog_table_content').height());
892			// jQuery($('admin_dialog_content').contentWindow.document.html).height($('admin_dialog_table_content').height);
893			// jQuery($('admin_dialog_content').contentWindow.document).find('iframe').each(function(){
894			// 	if( this.id && this.parentNode.tagName != 'TD' && jQuery(this).position().top > 0 ) {
895			// 		jQuery(this).height(jQuery('#admin_dialog_content').height()-jQuery(this).position().top);
896			// 	}
897			// });
898		},
899		onload:function(){
900			if(jQuery('#admin_dialog .dialog_close').find('svg').length == 0 ){
901				var svg = '<a class="cms-dialog-top-close">'
902					+'<svg class="icon--color-dark">'
903					+'<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-close"></use>'
904					+'</svg></a>';
905				jQuery('#admin_dialog .dialog_close').css('background','none').append(svg);
906			}
907			$('admin_dialog_content').height = $('admin_dialog_table_content').height;
908			var $iframe = jQuery($('admin_dialog_content').contentWindow.document);
909			var frameHeight = jQuery('#admin_dialog_table_content').height();
910			var height = frameHeight-$iframe.find('#cms_navigation_main .cms-navigation-wrap--head').height();
911			$iframe.find('.cms-navigation-wrap__sub-scroll').height(height);
912			var addHeight = 0;
913			if($iframe.find('.cms-navigation-nav__table').length > 0 ) addHeight = $iframe.find('.cms-navigation-nav__table').css('padding').replace('px','') * 2;
914			// var height = frameHeight-$iframe.find('#cms_navigation_main>.cms-table-list').height()-$iframe.find('#cms_navigation_main .cms-navigation-wrap--head').height() - addHeight;
915			// var height = frameHeight-$iframe.find('.cms-navigation-wrap__main').height()-$iframe.find('#cms_navigation_main .cms-table-list').height() - addHeight - 200;
916			var addHeight = 0;
917			if($iframe.find('.cms-navigation-nav__table').length > 0 ){
918				addHeight = $iframe.find('.cms-navigation-nav__table').css('padding').replace('px','') * 2;
919			}
920			if($iframe.find('.cms-navigation-wrap__main').length > 0){
921				addHeight += $iframe.find('.cms-navigation-wrap__main').height();
922			}
923			if($iframe.find('#cms_navigation_main .cms-table-list--scroll').length > 0){
924				addHeight += $iframe.find('#cms_navigation_main .cms-table-list--scroll').height();
925			}
926			var height = frameHeight - addHeight;
927			$iframe.find('#cms_navigation_list').height(height);
928			jQuery($('admin_dialog_content').contentWindow.document).find('#cms_dialog_container').height(jQuery('#admin_dialog_table_content').height());
929			jQuery($('admin_dialog_content').contentWindow.document.html).height($('admin_dialog_table_content').height);
930			jQuery($('admin_dialog_content').contentWindow.document).find('iframe').each(function(){
931// console.log(jQuery(this).position().top);
932				if( this.id && this.parentNode.tagName != 'TD' && jQuery(this).position().top > 0) {
933					jQuery(this).height(jQuery('#admin_dialog_content').height()-jQuery(this).position().top);
934				}
935			});
936			// jQuery.get('https://manual-v3.secure-cms.net/category.php?c='+win.options.title,function(c){
937			// 	if(c && jQuery('.cms-helpsite-btn').length == 0){
938			// 		var html = ''
939			// 		// +'<a href="#" onclick="alert(1);" class="">'
940			// 		+'<a href="#" style="z-index:1000000" onclick="alert(1);/* OpenSubWin(\'/_administrator/editor/help'+c+'\', 767, 860,\'help\');*/" class="cms-helpsite-btn cms-dialog-top-help">'
941			// 		// + "testwerwerwerwerwrwwe</a>";
942			// 		+'<svg class="icon--color-dark" onclick="alert(2);">'
943			// 		+'<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-help"></use>'
944			// 		+'</svg></a><script>alert(3);</script>';
945			// 		// jQuery('#admin_dialog_top').append(html);
946			// 		var a = document.createElement('span');
947			// 		a.href="javascript:;";
948			// 		a.className = 'cms-helpsite-btn cms-dialog-top-help';
949			// 		a.onclick = function(){ OpenSubWin('/_administrator/editor/help'+c,767,860,'help'); return false; };
950			// 		a.onmousedown = function(){ event.stopPropagation();};
951			// 		var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
952			// 		svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
953			// 		svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
954			// 		svg.setAttribute("class", "icon--color-dark");
955			// 		svg.innerHTML = '<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-help"></use>';
956			// 		a.append(svg);
957			// 		jQuery('#admin_dialog_top').append(a);
958			// 		// jQuery('.cms-dialog-top-help').on('click', function(event) {
959			// 		// 	console.log('test');
960			// 		// 	OpenSubWin('/_administrator/editor/help'+c+'/', 767, 860,'help');
961			// 		// 	return false;
962			// 		// });
963			// 	}
964			// });
965		}
966	});
967	if( status ) win.setStatusBar(status);
968	if( !reload ) win.setCloseCallback(windowReload);
969	if( mobile )
970	{
971		win.show(modal);
972	} else {
973		win.showCenter(modal);
974	}
975	// win.setURL(url);
976	set_cookie(cookie_first_url, url);
977}
978
979//sidebar.class.phpに移動.
980/*
981var barid = "_cms_sidebar";
982function open_side_bar(url, title, position)
983{
984	var sidebar = '<div id="'+barid+'"></div>';
985	if(url.match(/open_sidebar_for_parts/))
986		jQuery('html').css("overflow","hidden");
987	jQuery.ajax(url,{
988		type: 'get',
989		dataType: 'text',
990		cache: false,
991		timeout: 7000
992	}).done(function(data){
993		//console.log(data);
994		if(data == 'error')
995		{
996			alert_dialog('通信エラーが発生しました。再度お試しください。');
997			jQuery('html').css("overflow","auto");
998			context_menu.init();
999			return false;
1000		}
1001		
1002		context_menu.clear_event();
1003		if(jQuery('.cms-mouseover-infobox').length != 0)
1004			jQuery('.cms-mouseover-infobox').remove();
1005		
1006		if(jQuery('#'+barid).length == 0)
1007			jQuery('body').append(sidebar);
1008		jQuery('#'+barid).css({'position':'fixed','top':'0','z-index':'100001'});
1009		jQuery('#'+barid).css(position,'0');
1010		jQuery('#'+barid).html(data);
1011		
1012		if(jQuery('i.fa-times').length != 0)
1013		{
1014			jQuery('i.fa-times').on('click', function() {
1015				jQuery('#'+barid).remove();
1016				jQuery('html').css("overflow","auto");
1017				context_menu.init();
1018			});
1019		}
1020	}).fail(function() {
1021		alert_dialog('通信エラーが発生しました。再度お試しください。');
1022		jQuery('html').css("overflow","auto");
1023		context_menu.init();
1024		return false;
1025	});
1026	set_cookie(cookie_first_url, url);
1027}
1028*/
1029/*Cookie Consent Banner*/
1030function cookie_callback(response) {
1031	var message = response[0].cookie_consent_message;
1032	var cookie_consent = localStorage.getItem('_cms_cookie_consent');
1033	var display_flag = response[0].cookie_consent_popup_flag;
1034
1035	if(display_flag === '1' && cookie_consent === null){
1036		display_cookie_banner(message);
1037	} else if(display_flag === '1' && new Date().getTime() > JSON.parse(cookie_consent).dateExpire){
1038		display_cookie_banner(message);
1039	} else {
1040		return false;
1041	}
1042}
1043
1044function display_cookie_banner(usermessage) {
1045	var bodies = document.querySelectorAll('body');
1046	var body;
1047	for(var ii=0;ii<bodies.length;ii++){
1048		if(bodies[ii].id !== 'dialog'){
1049			body = bodies[ii];
1050			break;
1051		}
1052	}
1053
1054	if (!!body && body.querySelectorAll('#cms_cookieBanner').length < 1 ) {
1055		var banner = document.createElement('div');
1056		var closeContainer = document.createElement('div');
1057		var closeButton = document.createElement('button');
1058		var closeIcon = document.createElement('span');
1059		var textContainer = document.createElement('div');
1060		var acceptButtonContainer = document.createElement('div');
1061		var acceptButton = document.createElement('button');
1062
1063		banner.id = 'cms_cookieBanner';
1064		//banner.className = 'background--color-primary';
1065		//banner.style.minHeight = '150px';
1066		//banner.style.position = 'fixed';
1067		//banner.style.right = '0';
1068		//banner.style.bottom = '0';
1069		//banner.style.width = '100%';
1070		//banner.style.zIndex = '10000';
1071		//closeContainer.style.padding = '10px';
1072		//closeContainer.style.cursor = 'pointer';
1073		//closeContainer.style.width = '36px';
1074		//closeContainer.style.float = 'right';
1075		closeContainer.id = 'cms_cookieclose';
1076		closeContainer.addEventListener("click", set_consent_cookie);
1077		closeButton.type = 'button';
1078		//closeButton.className = 'close';
1079		closeIcon.innerHTML = '&times;';
1080		textContainer.className = 'cms_cookieMessage';
1081		textContainer.innerHTML = usermessage;
1082		//textContainer.style.textAlign = 'center';
1083		//textContainer.style.paddingTop = '20px';
1084		//textContainer.style.paddingBottom = '20px';
1085		acceptButtonContainer.id ='cms_cookieaccept_container';
1086		acceptButton.innerHTML = '同意する';
1087		acceptButton.type = 'button';
1088		acceptButton.className = 'btn';
1089		acceptButton.id ='cms_cookieaccept';
1090		acceptButton.addEventListener("click", set_consent_cookie);
1091		body.appendChild(banner);
1092		banner.appendChild(closeContainer);
1093		banner.appendChild(textContainer);
1094		banner.appendChild(acceptButtonContainer);
1095		acceptButtonContainer.appendChild(acceptButton);
1096		closeContainer.appendChild(closeButton);
1097		closeButton.appendChild(closeIcon);
1098	}
1099}
1100
1101function set_consent_cookie(e) {
1102	var cms_cookie_consent;
1103	var cookieBanner = jQuery('body #cms_cookieBanner');
1104	var now = new Date().getTime();
1105	var oneYearFromNow = new Date(new Date().setFullYear(new Date().getFullYear() + 1)).getTime();
1106
1107	if(e.target.id === 'cms_cookieaccept') {
1108		cms_cookie_consent = { userCookieConsent: true, consentDate: now, dateExpire: oneYearFromNow };
1109		localStorage.setItem('_cms_cookie_consent', JSON.stringify(cms_cookie_consent));
1110	} else if (e.target.matches('#cms_cookieclose') || e.target.matches('#cms_cookieclose *')) {
1111		cms_cookie_consent = {userCookieConsent: false, consentDate: null, dateExpire: now};
1112		localStorage.setItem('_cms_cookie_consent', JSON.stringify(cms_cookie_consent));
1113	}
1114	cookieBanner.fadeOut('slow');
1115}
1116
1117var get_cookie_banner_settings = (function(){
1118	var executed = false;
1119	return function(){
1120		if(!executed){
1121			executed = true;
1122			jQuery.ajax({
1123				type: "GET",
1124				dataType: "json",
1125				url: "/index.php?a=free_page/get_cookie_policy_setting/"
1126			}).done((data) => {
1127				cookie_callback(data);
1128			}).fail((data) => {
1129				//console.log('fail');
1130				//console.log(data.statusText, ' has occurred');
1131			});
1132		}
1133	}
1134})();
1135//get_cookie_banner_settings();
1136
1137var reload_flag=false;
1138function windowReload(param)
1139{
1140	param.options.url=false;
1141	/*location.reload();*/
1142	if( reload_flag !== false )
1143	{
1144		if( reload_flag === 'ajax' )
1145		{
1146			info_dialog('更新中...', 200, 100, true);
1147			new Ajax.Updater(
1148				{success:'cms_contents_area'},
1149				location.href, {
1150					method: "get",
1151					parameters : 'cmsmode=reload',
1152					evalScripts: true,
1153					onFailure:function(httpObj){
1154						$(elm).innerHTML = "Error!!";
1155					},
1156					onSuccess:function(httpobj){
1157						/*
1158						setTimeout(function(){
1159							Dialog.closeInfo();
1160							const contents = document.querySelector('body #sitedesign-pagetop');
1161							if(contents){
1162								contents.style.opacity = '1';
1163							}
1164						},1200);
1165						*/
1166						Dialog.closeInfo();
1167					}
1168				}
1169			);
1170			reload_flag = false;
1171		} else if( reload_flag === true || reload_flag === 'reload' ) {
1172			location.reload();
1173		}
1174	}
1175	if( typeof context_menu == 'object' ) context_menu.init();
1176	return true;
1177}
1178function windowClose()
1179{
1180	return true;
1181}
1182var debug = {
1183	win: null,
1184	p: function(str){
1185		if( !debug.win ) {
1186			debug.o();
1187		}
1188		debug.win.document.write(str+"<br>");
1189	},
1190	o: function(){
1191		debug.win = window.open('about:blank','debug','width=300,height=300,resizable=yes');
1192	}
1193};
1194var target_element=null;
1195function showFileManager(dir, element_name, easy_edit_mode=null){
1196	if(typeof context_menu !== 'undefined' && easy_edit_mode != null){
1197		context_menu.easy_edit_mode = easy_edit_mode;
1198	}
1199	if( !element_name ) element_name = null;
1200	target_element = element_name;
1201	var url = "/_administrator/editor/FCKeditor/editor/filemanager/browser/mcpuk/browser.html?Type=File&Connector=connectors/php/connector.php";
1202	if( dir ){
1203		url += '&CurrentFolder=' + dir.replace(/\/+/,"/");
1204	} else if( get_cookie('close_file_dir') !== ''  ) {
1205		url += '&CurrentFolder=' + get_cookie('close_file_dir').replace(/\/+/,"/");
1206	}
1207	x=800;
1208	y=600;
1209	x = screen.width * .7;
1210	y = (screen.height - 50) * .8;
1211	OpenSubWin(url,x,y,'server_browser');
1212}
1213function showImageManager(dir, element_name, easy_edit_mode=null){
1214	if(typeof context_menu !== 'undefined' && easy_edit_mode != null){
1215		context_menu.easy_edit_mode = easy_edit_mode;
1216	}
1217	if( !element_name ) element_name = null;
1218	target_element = element_name;
1219	if (element_name !== null && element_name.startsWith('cf-')){
1220		var url = "/_administrator/editor/FCKeditor/editor/filemanager/browser/mcpuk/browser.html?Connector=connectors/php/connector.php";
1221	} else {
1222		var url = "/_administrator/editor/FCKeditor/editor/filemanager/browser/mcpuk/browser.html?Type=Image&Connector=connectors/php/connector.php";
1223	}
1224	if( dir ){
1225		url += '&CurrentFolder=' + dir.replace(/\/+/,"/");
1226	} else if( get_cookie('close_image_dir') !== ''  ) {
1227		url += '&CurrentFolder=' + get_cookie('close_image_dir').replace(/\/+/,"/");
1228	}
1229	x=800;
1230	y=600;
1231	x = screen.width * .7;
1232	y = (screen.height - 50) * .8;
1233	OpenSubWin(url,x,y,'server_browser');
1234}
1235function SetUrl( fileUrl ){
1236
1237	if( !!target_element && !!$(target_element) ) {
1238		$(target_element).value = fileUrl;
1239		if(!!$(target_element).onchange) $(target_element).onchange();
1240
1241	} else if( typeof context_menu == 'object' && !!context_menu.easy_edit_mode ) {
1242		context_menu.easy_edit_mode_image(fileUrl);
1243	} else {
1244		OpenSubWin(fileUrl,600,480,"image");
1245		return true;
1246	}
1247}
1248function OpenSubWin(url, x, y,winname ){
1249	if( !url ) return false;
1250	if( !winname ) winname = 'subwin';
1251	var option = '';
1252	if( !mobile )
1253	{
1254		if( !x ) x = 600;
1255		if( !y ) y = 480;
1256		wx = screen.width;
1257		wy = screen.height - 50;
1258		dx  = ( wx - x ) / 2;
1259		dy   = ( wy - y ) / 2;
1260		option  = "top=" + dy + 'px';
1261		option += ",left=" + dx + 'px';
1262		option += ",width=" + x + 'px';
1263		option += ",height=" + y + 'px';
1264		option += ",scrollbars=yes,status=yes,resizable=yes,chrome=yes,centerscreen=yes";
1265		w = window.open(url,winname,option);
1266		// w.moveTo(dx,dy);
1267		w.focus();
1268
1269		// 何もせずサーバーブラウザを閉じた場合の初期化
1270		const timer = setInterval(() => {
1271			if (w.closed) {
1272				clearInterval(timer);
1273				context_menu.init();
1274			}
1275		}, 1000);
1276	}
1277	else
1278	{
1279		window.open(url,winname,option);
1280	}
1281}
1282function screenInfomation(){
1283	var s = new Object();
1284	var p;
1285	try {
1286		p = parent.document;
1287		s.width     = p.documentElement.clientWidth||p.body.clientWidth;    // 横幅
1288		s.nowHeight = p.documentElement.clientHeight;    // 現在表示している画面の高さ
1289		s.height    = p.documentElement.scrollHeight||p.body.clientHeight;    // 画面の高さ
1290		s.x = p.documentElement.scrollLeft||p.body.scrollLeft;    // 横の移動量
1291		s.y = p.documentElement.scrollTop||p.body.scrollTop;      // 縦の移動量
1292	} catch(e) {
1293		s.width     = document.body.clientWidth;
1294		s.nowHeight = document.body.clientHeight;
1295		s.height    = window.screen.height - 200;
1296		s.x = document.body.scrollLeft;
1297		s.y = document.body.scrollTop;
1298		p = window.document;
1299		s.width     = p.documentElement.clientWidth||p.body.clientWidth;    // 横幅
1300		s.nowHeight = p.documentElement.clientHeight;    // 現在表示している画面の高さ
1301		s.height    = p.documentElement.scrollHeight||p.body.clientHeight;    // 画面の高さ
1302		s.x = p.documentElement.scrollLeft||p.body.scrollLeft;    // 横の移動量
1303		s.y = p.documentElement.scrollTop||p.body.scrollTop;      // 縦の移動量
1304
1305
1306	}
1307	return s;
1308}
1309
1310function get_brouser_height(){
1311	// IE以外。
1312	if (!document.all && (document.layers || document.getElementById)) {
1313	    return window.innerHeight;
1314	    }
1315	// ウィンドウズIE 6・標準モード。
1316	else if (document.getElementById && (document.compatMode=='CSS1Compat')) {
1317	    return document.documentElement.clientHeight;
1318	    }
1319	// その他のIE。
1320	else if (document.all) {
1321	    return document.body.clientHeight;
1322	    }
1323	// その他(非対応)。
1324	else {
1325	    return 700;
1326	    }
1327}
1328function get_brouser_width(){
1329	// IE以外。
1330	if (!document.all && (document.layers || document.getElementById)) {
1331	    return window.innerWidth;
1332	    }
1333	// ウィンドウズIE 6・標準モード。
1334	else if (document.getElementById && (document.compatMode=='CSS1Compat')) {
1335	    return document.documentElement.clientWidth;
1336	    }
1337	// その他のIE。
1338	else if (document.all) {
1339	    return document.body.clientWidth;
1340	    }
1341	// その他(非対応)。
1342	else {
1343	    return 1024;
1344	    }
1345}
1346function isNumeric(num){
1347	var numeric="0123456789";
1348	if(num.length==0){
1349		return true;
1350	}
1351	for(i=0;i < num.length;i++){
1352		if(numeric.indexOf(num.charAt(i))<0){
1353			return false;
1354		}
1355	}
1356	return true;
1357}
1358/**
1359* デバッグ用
1360*
1361**/
1362if (!('console' in window)) {
1363
1364  window.console = {};
1365  window.console.log = function(str){
1366      return str;
1367  };
1368  window.console.warn = function(str){
1369      return str;
1370  };
1371  window.console.info = function(str){
1372      return str;
1373  };
1374  window.console.error = function(str){
1375      return str;
1376  };
1377}
1378function _uc(str) {
1379	return encodeURIComponent(str);
1380}
1381function _u(str) {
1382	return encodeURI(str);
1383}
1384function _us(str) {
1385	return encodeURI(str);
1386}
1387function _h(str) {
1388	return html_escape(str);
1389}
1390function _j(str) {
1391	return escape(str);
1392}
1393function _ba(str) {
1394	return window.btoa(unescape(_uc(str)))
1395}
1396function _raw(str) {
1397	return str;
1398}
1399function html_escape(str) {
1400	if( typeof str === 'number' ) return str;
1401	if(typeof str !== 'string') throw 'invalid parameter:'+str;
1402	return str.replace(/[&'`"<>]/g, function (match) { //'
1403		return {
1404			'&': '&amp;',
1405			"'": '&#x27;',
1406			'`': '&#x60;',
1407			'"': '&quot;',
1408			'<': '&lt;',
1409			'>': '&gt;'
1410		}[match]
1411	});
1412}
1413//Javascript版preg_quote.
1414function preg_quote(str, delimiter){
1415    // Quote regular expression characters plus an optional character  
1416    // 
1417    // version: 1107.2516
1418    // discuss at: http://phpjs.org/functions/preg_quote
1419    // +   original by: booeyOH
1420    // +   improved by: Ates Goral (http://magnetiq.com)
1421    // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
1422    // +   bugfixed by: Onno Marsman
1423    // +   improved by: Brett Zamir (http://brett-zamir.me)
1424    // *     example 1: preg_quote("$40");
1425    // *     returns 1: '\$40'
1426    // *     example 2: preg_quote("*RRRING* Hello?");
1427    // *     returns 2: '\*RRRING\* Hello\?'
1428    // *     example 3: preg_quote("\\.+*?[^]$(){}=!<>|:");
1429    // *     returns 3: '\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:'
1430    return (str + '').replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&');
1431}
1432
1433if (typeof globalThis.HelperSetButton !== 'function') {
1434	/**
1435	 * ボタンのHTML生成を汎用的に行うヘルパークラス
1436	 *
1437	 * usage.
1438	 *   top.buttonHelper = new HelperSetButton();
1439	 *   top.buttonHelper.rewriteClickEvent = function(b, evSeed) {
1440	 *       if (b === 'ok') {
1441	 *           evSeed.jsOnclick = 'buttonHelper.saveImgPropSubmit();';
1442	 *       }
1443	 *   }
1444	 *   top.buttonHelper.saveImgPropSubmit = function() {
1445	 *       xxxx
1446	 *   }
1447	 *   top.buttonHelper.setButton({ok: 1, cancel: 1, save: 1});
1448	 */
1449	globalThis.HelperSetButton = class generateContentsHelper {
1450
1451		// 追加ボタン
1452		addSeedBtn() {
1453			return {};
1454		}
1455
1456		getSeedBtn() {
1457			const iniSeed = {
1458				ok: ({
1459					btnClass: 'cms_btn--ok_return',
1460					icon: 'icon-ok',
1461					label: 'OK',
1462					jsOnclick: 'admin_dialog_content.CMSAdmin.submit_close();'
1463				}),
1464				cancel: ({
1465					btnClass: 'cms_btn--cancel',
1466					icon: 'icon-cancel',
1467					label: 'キャンセル',
1468					jsOnclick: 'admin_dialog_content.CMSAdmin.close();'
1469				}),
1470				close: ({
1471					btnClass: 'cms_btn--close',
1472					icon: 'icon-cancel',
1473					label: '閉じる',
1474					jsOnclick: 'admin_dialog_content.CMSAdmin.close();'
1475				}),
1476				save: ({
1477					btnClass: 'cms_btn--save',
1478					icon: 'icon-save',
1479					label: '保存',
1480					jsOnclick: 'admin_dialog_content.CMSAdmin.submit();'
1481				}),
1482				back_return: ({
1483					btnClass: 'cms_btn--top',
1484					icon: 'icon-back',
1485					label: '戻る',
1486					jsOnclick: 'admin_dialog_content.resume_result();'
1487				}),
1488			};
1489
1490			// マージ
1491			return {
1492				...iniSeed,
1493				...this.addSeedBtn()
1494			};
1495		}
1496
1497		safeConvBtnSeed(seed) {
1498			return JSON.parse(JSON.stringify(seed));
1499		}
1500
1501		makeBtn(seed_btn) {
1502			const iconId = seed_btn.icon;
1503			if (!/^[a-zA-Z0-9_-]+$/.test(iconId)) {
1504				console.error('Invalid icon ID: Unsafe characters detected in seed_btn.icon.');
1505				return false;
1506			}
1507
1508			const htmlString = `
1509				<a href="#" class="cms_btn" alt="" onclick="return false;">
1510					<svg><use xlink:href=""></use></svg>
1511				</a>
1512			`.trim();
1513
1514			// 文字列であるhtmlタグをDOM要素に変換
1515			const tmpTag = document.createElement('div');
1516			tmpTag.insertAdjacentHTML('afterbegin', htmlString);
1517			const aTag = tmpTag.firstElementChild;
1518			if (!aTag || aTag.tagName !== 'A') {
1519				console.error('Failed to create <a> element.');
1520				return null;
1521			}
1522
1523			// 各値をセット
1524			aTag.classList.add(seed_btn.btnClass);
1525			aTag.setAttribute('alt', seed_btn.label);
1526			aTag.setAttribute('onclick', `${seed_btn.jsOnclick} return false;`);
1527			aTag.querySelector('svg').after(document.createTextNode(seed_btn.label));
1528
1529			const useHref = `/_administrator/css/default/images/svg/symbol-defs.svg#${iconId}`
1530			aTag.querySelector('use').setAttribute('xlink:href', useHref);
1531
1532			return aTag;
1533		}
1534
1535		// イベント上書き
1536		rewriteClickEvent(b, evSeed) {
1537			// ▼記載サンプル
1538			// if (b === 'ok') {
1539			// 	evSeed.jsOnclick = 'admin_dialog_content.CMSAdmin.submit_close();';
1540			// }
1541			// else if (b === 'cancel') {
1542			// 	evSeed.jsOnclick = 'admin_dialog_content.CMSAdmin.cancel();';
1543			// }
1544		}
1545
1546		getBtnElements(button) {
1547			const seed_btn_defaults = this.getSeedBtn();
1548			let btn_elements = [];
1549			let index = 0;
1550
1551			for (let b in button) {
1552				if (Object.prototype.hasOwnProperty.call(button, b)) {
1553					if ( !seed_btn_defaults.hasOwnProperty(b) ) {
1554						continue;
1555					}
1556
1557					let evSeed = this.safeConvBtnSeed(seed_btn_defaults[b]);
1558
1559					// イベント上書き
1560					this.rewriteClickEvent(b, evSeed);
1561
1562					const btnElement = this.makeBtn(evSeed);
1563					if (btnElement) {
1564						btn_elements[index] = btnElement;
1565						index++;
1566					}
1567				}
1568			}
1569			return btn_elements.reverse();
1570		}
1571
1572		run(viewBtnMany) {
1573			const targetElement = parent.document.getElementById('cms_dialog_ctrl_td');
1574			if ( !targetElement ) {
1575				console.error('Target element "cms_dialog_ctrl_td" not found.');
1576				return;
1577			}
1578
1579			targetElement.textContent = '';
1580			this.getBtnElements(viewBtnMany).forEach(element => {
1581				targetElement.append(element);
1582			});
1583
1584			try {
1585				const winobj = top.Windows.getFocusedWindow();
1586				if (winobj && typeof winobj.setStatusBar === 'function') {
1587					winobj.setStatusBar('');
1588				}
1589			}
1590			catch (e) {
1591				console.warn('Could not set status bar. Window object manipulation failed.', e);
1592			}
1593		}
1594	}
1595}
1596/**
1597 * AI Content Generation - 広告管理モジュール
1598 * Google AdSense / Google Ad Manager (IMA SDK) の初期化・管理を行う
1599 */
1600if (typeof globalThis.AiAdManager !== 'function') {
1601
1602    globalThis.AiAdManager = class AiAdManager {
1603        #adProvider = 'admanager';
1604        #adVastTag = '';
1605        #adsenseClientId = '';
1606        #adsenseSlotId = '';
1607        #adsenseAdType = 'auto';
1608        #adsenseResponsive = false;
1609        #adsenseCloseButtonDelay = 5;
1610
1611        #adContainerOverlay = null;
1612        #adContainer = null;
1613        #closeButton = null;
1614        #countdownTimer = null;
1615        #adsManager = null;
1616        #adsLoader = null;
1617        #zIndexBase = 300000;
1618
1619        #onAdCompleted = null;
1620
1621        /**
1622         * @param {Object} config - 広告設定
1623         * @param {string} config.adProvider - 'adsense' または 'admanager'
1624         * @param {string} config.adVastTag - Ad Manager用VASTタグURL
1625         * @param {string} config.adsenseClientId - AdSense クライアントID
1626         * @param {string} config.adsenseSlotId - AdSense スロットID
1627         * @param {string} config.adsenseAdType - AdSense 広告タイプ
1628         * @param {boolean} config.adsenseResponsive - AdSense レスポンシブ設定
1629         * @param {number} config.adsenseCloseButtonDelay - AdSense 閉じるボタン表示までの秒数
1630         */
1631        constructor(config = {}) {
1632            this.#adProvider = config.adProvider || 'admanager';
1633            this.#adVastTag = config.adVastTag || '';
1634            this.#adsenseClientId = config.adsenseClientId || '';
1635            this.#adsenseSlotId = config.adsenseSlotId || '';
1636            this.#adsenseAdType = config.adsenseAdType || 'auto';
1637            this.#adsenseResponsive = config.adsenseResponsive || false;
1638            this.#adsenseCloseButtonDelay = config.adsenseCloseButtonDelay ?? 5;
1639        }
1640
1641        /**
1642         * zIndexのベース値を設定
1643         * @param {number} zIndex
1644         */
1645        setZIndexBase(zIndex) {
1646            this.#zIndexBase = zIndex;
1647        }
1648
1649        /**
1650         * 広告完了時のコールバックを設定
1651         * @param {Function} callback
1652         */
1653        setOnAdCompleted(callback) {
1654            this.#onAdCompleted = callback;
1655        }
1656
1657        /**
1658         * 広告SDKを読み込む
1659         */
1660        loadAdSdk() {
1661            if (this.#adProvider === 'adsense') {
1662                this.#loadAdSenseScript();
1663            } else {
1664                this.#loadImaSdk();
1665            }
1666        }
1667
1668        /**
1669         * 広告コンテナを新規作成
1670         */
1671        newAdContainer() {
1672            this.removeAdContainer();
1673
1674            window.top.document.body.insertAdjacentHTML('beforeend', this.#templateAdContainer());
1675            this.#adContainerOverlay = window.top.document.getElementById('ai_modal_ad_overlay');
1676            this.#adContainerOverlay.style.zIndex = this.#zIndexBase + 3;
1677            this.#adContainer = window.top.document.getElementById('ai_modal_ad_container');
1678        }
1679
1680        /**
1681         * 広告コンテナを削除
1682         */
1683        removeAdContainer() {
1684            // カウントダウンタイマーをクリア
1685            if (this.#countdownTimer) {
1686                clearInterval(this.#countdownTimer);
1687                this.#countdownTimer = null;
1688            }
1689
1690            // プロバイダ別の destroy 処理
1691            if (this.#adProvider === 'adsense') {
1692                // AdSense: 特別な destroy 処理は不要(DOM削除で自動クリーンアップ)
1693            } else {
1694                // AdManager: IMA adsManager を destroy
1695                this.#adsManager?.destroy();
1696            }
1697
1698            // コンテナを DOM から削除
1699            this.#adContainerOverlay?.remove();
1700            this.#adContainerOverlay = null;
1701            this.#adContainer = null;
1702            this.#closeButton = null;
1703        }
1704
1705        /**
1706         * 広告コンテナを表示
1707         */
1708        showAdContainer() {
1709            if (this.#adContainerOverlay) {
1710                this.#adContainerOverlay.style.display = 'block';
1711            }
1712        }
1713
1714        /**
1715         * 広告を初期化して表示
1716         */
1717        initializeAds() {
1718            if (this.#adProvider === 'adsense') {
1719                this.#initializeAdSense();
1720            } else {
1721                this.#initializeIMA();
1722            }
1723        }
1724
1725        // ========== プライベートメソッド: IMA SDK (Ad Manager) ==========
1726
1727        #initializeIMA() {
1728            try {
1729                const adDisplayContainer = new window.google.ima.AdDisplayContainer(
1730                    this.#adContainer,
1731                );
1732                adDisplayContainer.initialize();
1733
1734                this.#adsLoader = new window.google.ima.AdsLoader(adDisplayContainer);
1735
1736                this.#adsLoader.addEventListener(
1737                    window.google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
1738                    this.#onAdsManagerLoaded,
1739                    false,
1740                );
1741
1742                this.#adsLoader.addEventListener(
1743                    window.google.ima.AdErrorEvent.Type.AD_ERROR,
1744                    (adErrorEvent) => {
1745                        const err = adErrorEvent.getError();
1746                        console.error('広告エラー:', err.getMessage(), err.getVastErrorCode?.()
1746);
1747                    },
1748                    false,
1749                );
1750
1751                // VAST広告タグをリクエスト
1752                const adsRequest = new window.google.ima.AdsRequest();
1753                adsRequest.adTagUrl = this.#adVastTag;
1754
1755                this.#adsLoader.requestAds(adsRequest);
1756
1757            } catch (e) {
1758                console.error('IMA初期化エラー:', e);
1759                throw e;
1760            }
1761        }
1762
1763        #onAdsManagerLoaded = (adsManagerLoadedEvent) => {
1764            this.#adsManager = adsManagerLoadedEvent.getAdsManager();
1765
1766            this.#adsManager.addEventListener(
1767                window.google.ima.AdErrorEvent.Type.AD_ERROR,
1768                (adErrorEvent) => {
1769                    const err = adErrorEvent.getError();
1770                    console.error('広告エラー:', err.getMessage(), err.getVastErrorCode?.());
1771                },
1772            );
1773
1774            // 広告開始イベント
1775            this.#adsManager.addEventListener(
1776                window.google.ima.AdEvent.Type.STARTED,
1777                () => {
1778                    // console.log('広告が開始されました');
1779                }
1780            );
1781
1782            // 広告完了イベント
1783            this.#adsManager.addEventListener(
1784                window.google.ima.AdEvent.Type.ALL_ADS_COMPLETED,
1785                () => {
1786                    // console.log('広告表示が完了しました');
1787                    this.removeAdContainer();
1788                    this.#onAdCompleted?.();
1789                }
1790            );
1791
1792            // 広告スキップイベント
1793            this.#adsManager.addEventListener(
1794                window.google.ima.AdEvent.Type.SKIPPED,
1795                () => {
1796                    // console.log('広告がスキップされました');
1797                    this.removeAdContainer();
1798                    this.#onAdCompleted?.();
1799                }
1800            );
1801
1802            // クリックされたとき
1803            this.#adsManager.addEventListener(
1804                window.google.ima.AdEvent.Type.CLICK,
1805                () => {
1806                    this.removeAdContainer();
1807                    this.#onAdCompleted?.();
1808                },
1809            );
1810
1811            try {
1812                this.#adsManager.init(this.#adContainer.clientWidth, this.#adContainer.clientHeight, window.google.ima.ViewMode.NORMAL);
1813                this.#adsManager.setVolume(0); // ミュート設定
1814                this.#adsManager.start();
1815                // console.log('IMA広告が正常に開始されました');
1816            } catch (adError) {
1817                console.error('Failed to start IMA ads:', adError);
1818            }
1819        }
1820
1821        #loadImaSdk() {
1822            // すでに読み込み済みなら二重読み込みしない
1823            if (window.google && window.google.ima) {
1824                return;
1825            }
1826
1827            const script = window.top.document.createElement('script');
1828            script.src = 'https://imasdk.googleapis.com/js/sdkloader/ima3.js';
1829            script.async = true;
1830
1831            script.onload = () => {
1832                window.google = window.top.window.google;
1833            };
1834
1835            script.onerror = () => {
1836                console.error('Failed to load IMA SDK');
1837            };
1838
1839            window.top.document.head.appendChild(script);
1840        }
1841
1842        // ========== プライベートメソッド: AdSense ==========
1843
1844        #initializeAdSense() {
1845            try {
1846                if (!this.#adsenseClientId || !this.#adsenseSlotId) {
1847                    console.warn('AdSense設定が不完全です。クライアントIDとスロットIDを指定してください。');
1848                    return;
1849                }
1850
1851                // AdSense の <ins> タグを準備
1852                const insTag = this.#adContainer.querySelector('ins.adsbygoogle');
1853                if (!insTag) {
1854                    console.error('AdSense <ins> タグが見つかりません');
1855                    return;
1856                }
1857
1858                // 属性設定
1859                insTag.setAttribute('data-ad-client', this.#adsenseClientId);
1860                insTag.setAttribute('data-ad-slot', this.#adsenseSlotId);
1861                insTag.setAttribute('data-ad-format', this.#adsenseAdType);
1862                if (this.#adsenseResponsive) {
1863                    insTag.setAttribute('data-full-width-responsive', 'true');
1864                }
1865
1866                // AdSense はコンテナが表示状態でないと初期化できないため、先に表示する
1867                this.showAdContainer();
1868
1869                // 閉じるボタンの参照を取得し、イベントを設定
1870                this.#closeButton = this.#adContainerOverlay.querySelector('[data-name="ai_ad_close_button"]');
1871                if (this.#closeButton) {
1872                    this.#closeButton.addEventListener('click', this.#handleCloseButtonClick);
1873                }
1874
1875                // カウントダウンを開始
1876                this.#startCloseButtonCountdown();
1877
1878                // AdSense スクリプトが読み込まれるまで待機し、レイアウト計算完了後に初期化
1879                this.#waitForAdSenseScript(() => {
1880                    // レイアウト計算を確実に完了させるため、次のフレームまã
1880§å¾…機
1881                    requestAnimationFrame(() => {
1882                        try {
1883                            // 既存の広告をクリア(複数回初期化時対応)
1884                            (window.top.adsbygoogle = window.top.adsbygoogle || []).push({
1885                                google_ad_client: this.#adsenseClientId,
1886                                google_ad_slot: this.#adsenseSlotId,
1887                            });
1888                        } catch (e) {
1889                            console.error('AdSense 初期化エラー:', e);
1890                        }
1891                    });
1892                });
1893
1894            } catch (e) {
1895                console.error('AdSense 初期化失敗:', e);
1896            }
1897        }
1898
1899        /**
1900         * 閉じるボタンのカウントダウンを開始
1901         */
1902        #startCloseButtonCountdown() {
1903            if (!this.#closeButton) return;
1904
1905            let remainingSeconds = this.#adsenseCloseButtonDelay;
1906            const countdownText = this.#closeButton.querySelector('[data-name="ai_ad_countdown_text"]');
1907            const closeText = this.#closeButton.querySelector('[data-name="ai_ad_close_text"]');
1908
1909            // 初期表示
1910            if (countdownText) {
1911                countdownText.textContent = `${remainingSeconds}秒後に閉じられます`;
1912            }
1913
1914            this.#countdownTimer = setInterval(() => {
1915                remainingSeconds--;
1916
1917                if (remainingSeconds <= 0) {
1918                    // カウントダウン完了
1919                    clearInterval(this.#countdownTimer);
1920                    this.#countdownTimer = null;
1921
1922                    // 閉じるボタンを有効化
1923                    this.#closeButton.disabled = false;
1924                    this.#closeButton.classList.add('ai_ad_close_button_active');
1925
1926                    if (countdownText) countdownText.style.display = 'none';
1927                    if (closeText) closeText.style.display = 'inline';
1928                } else {
1929                    // カウントダウン更新
1930                    if (countdownText) {
1931                        countdownText.textContent = `${remainingSeconds}秒後に閉じられます`;
1932                    }
1933                }
1934            }, 1000);
1935        }
1936
1937        /**
1938         * 閉じるボタンクリック時の処理
1939         */
1940        #handleCloseButtonClick = () => {
1941            this.removeAdContainer();
1942            this.#onAdCompleted?.();
1943        }
1944
1945        #waitForAdSenseScript(callback, timeout = 5000) {
1946            const startTime = Date.now();
1947            const checkInterval = 100;
1948
1949            const checkAndExecute = () => {
1950                if (window.top.adsbygoogle !== undefined) {
1951                    callback();
1952                } else if (Date.now() - startTime < timeout) {
1953                    setTimeout(checkAndExecute, checkInterval);
1954                } else {
1955                    console.warn('AdSense スクリプトの読み込みがタイムアウトしました');
1956                }
1957            };
1958
1959            checkAndExecute();
1960        }
1961
1962        #loadAdSenseScript() {
1963            // すでに読み込み済みなら二重読み込みしない
1964            if (window.adsbygoogle) {
1965                return;
1966            }
1967
1968            const script = window.top.document.createElement('script');
1969            script.async = true;
1970            script.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js';
1971            script.setAttribute('data-ad-client', this.#adsenseClientId);
1972
1973            script.onload = () => {
1974                console.log('AdSense script loaded successfully');
1975            };
1976
1977            script.onerror = () => {
1978                console.error('Failed to load AdSense script');
1979            };
1980
1981            window.top.document.head.appendChild(script);
1982        }
1983
1984        // ========== プライベートメソッド: テンプレート ==========
1985
1986        #templateAdContainer() {
1987            if (this.#adProvider === 'adsense') {
1988                return this.#templateAdContainerAdSense();
1989            } else {
1990                return this.#templateAdContainerAdManager();
1991            }
1992        }
1993
1994        #templateAdContainerAdManager() {
1995            return `
1996			<div id="ai_modal_ad_overlay"
1997				style="
1998					position: fixed;
1999					inset: 0;
2000					background: rgba(0,0,0,.9);
2001					display: none;
2002				"
2003			>
2004				<div id="ai_modal_ad_container"
2005					style="
2006						position: absolute;
2007						top: 50%;
2008						left: 50%;
2009						transform: translate(-50%, -50%);
2010						width: 1080px;
2011						max-width: 90vw;
2012						max-height: 90vh;
2013						aspect-ratio: 16 / 9;
2014						background-color: black;
2015					"
2016				></div>
2017			</div>
2018		`;
2019        }
2020
2021        #templateAdContainerAdSense() {
2022            return `
2023			<div id="ai_modal_ad_overlay"
2024				style="
2025					position: fixed;
2026					inset: 0;
2027					background: rgba(0,0,0,.9);
2028					display: none;
2029				"
2030			>
2031				<button
2032					data-name="ai_ad_close_button"
2033					disabled
2034					style="
2035						position: absolute;
2036						top: 16px;
2037						right: 16px;
2038						padding: 8px 16px;
2039						border: none;
2040						border-radius: 4px;
2041						background-color: rgba(255, 255, 255, 0.3);
2042						color: #fff;
2043						font-size: 14px;
2044						cursor: not-allowed;
2045						transition: background-color 0.3s, cursor 0.3s;
2046						z-index: 1;
2047					"
2048				>
2049					<span data-name="ai_ad_countdown_text"></span>
2050					<span data-name="ai_ad_close_text" style="display: none;">✕ 閉じる</span>
2051				</button>
2052				<style>
2053					.ai_ad_close_button_active {
2054						background-color: rgba(255, 255, 255, 0.9) !important;
2055						color: #333 !important;
2056						cursor: pointer !important;
2057					}
2058					.ai_ad_close_button_active:hover {
2059						background-color: #fff !important;
2060					}
2061				</style>
2062				<div id="ai_modal_ad_container"
2063					style="
2064						position: absolute;
2065						top: 50%;
2066						left: 50%;
2067						transform: translate(-50%, -50%);
2068						width: 1080px;
2069						max-width: 90vw;
2070						max-height: 90vh;
2071						padding: 20px;
2072						background-color: #f0f0f0;
2073						border-radius: 8px;
2074						box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
2075						display: flex;
2076						align-items: center;
2077						justify-content: center;
2078						overflow: auto;
2079					"
2080				>
2081					<ins class="adsbygoogle"
2082						style="
2083							display: block;
2084							text-align: center;
2085							width: 100%;
2086							min-width: 300px;
2087							min-height: 250px;
2088						"
2089					></ins>
2090				</div>
2091			</div>
2092		`;
2093        }
2094    }
2095}
2096
2097
2098if (typeof globalThis.aiContentGenerator !== 'function') {
2099
2100	const aiContentGeneration_showAD = false;
2101	const aiContentGeneration_textLengthOptions = {"direct_edit_html":[30,200,500,1000,2000],"direct_edit_textbox":[40,100,200],"direct_edit_textarea":[40,200,400],"blog_title":[20,40],"blog_description":[100,200,400],"blog_entry_title":[20,40],"blog_entry_description":[100,200,400],"event_title":[20,40],"event_description":[100,200,400],"event_entry_title":[20,40],"event_entry_description":[100,200,400],"topics_title":[20,40],"topics_description":[100,200,400],"topics_entry_title":[20,40],"topics_entry_description":[100,200,400],"free_page_title":[20,40],"free_page_description":[100,200,400],"metatag_title":[20,40],"metatag_description":[100,200,400],"site_title":[20,40],"site_description":[100,200,400]};
2102	const aiContentGeneration_tones = ["\u30d5\u30a9\u30fc\u30de\u30eb","\u4e01\u5be7","\u30ab\u30b8\u30e5\u30a2\u30eb","\u67d4\u3089\u304b\u3044","\u529b\u5f37\u3044","\u5ba2\u89b3\u7684"];
2103	const aiContentGeneration_outputStyles = {"direct_edit_html":"html","direct_edit_textbox":"text","direct_edit_textarea":"text_with_only_br","blog_title":"text","blog_description":"text","blog_entry_title":"text","blog_entry_description":"text","event_title":"text","event_description":"text","event_entry_title":"text","event_entry_description":"text","topics_title":"text","topics_description":"text","topics_entry_title":"text","topics_entry_description":"text","free_page_title":"text","free_page_description":"text","metatag_title":"text","metatag_description":"text","site_title":"text","site_description":"text"};
2104
2105	// 広告プロバイダ設定
2106	const aiContentGeneration_adProvider = "adsense";
2107	const aiContentGeneration_adsenseClientId = "ca-pub-0300124455324951";
2108	const aiContentGeneration_adsenseSlotId = "7838772540";
2109	const aiContentGeneration_adsenseAdType = "auto";
2110	const aiContentGeneration_adsenseResponsive = true;
2111	const aiContentGeneration_adsenseCloseButtonDelay = 5;
2112	const aiContentGeneration_adVastTag = "";
2113	const aiContentGeneration_adminName = "";
2114	const aiContentGeneration_manualId = 'ゴーゴーAIで生成';
2115	const aiContentGeneration_helpCategoryApiURL = 'https\x3a\x2f\x2fmanual\x2dv3\x2esecure\x2dcms\x2enet\x2fcategory\x2ephp\x3fc\x3d' + encodeURIComponent(aiContentGeneration_manualId);
2116	const aiContentGeneration_videoManualApiURL = 'https\x3a\x2f\x2fmanual\x2dv3\x2esecure\x2dcms\x2enet\x2fvideomanual\x2ephp\x3fc\x3d' + encodeURIComponent(aiContentGeneration_manualId);
2117	const aiContentGeneration_helpPageBaseURL = '\x2f\x5fadministrator\x2feditor\x2fhelp';
2118
2119	globalThis.aiContentGenerator = class aiContentGenerator {
2120
2121		#showAD = aiContentGeneration_showAD;
2122		#adminName = aiContentGeneration_adminName;
2123		#adProvider = aiContentGeneration_adProvider;
2124		#adVastTag = aiContentGeneration_adVastTag;
2125		#adsenseClientId = aiContentGeneration_adsenseClientId;
2126		#adsenseSlotId = aiContentGeneration_adsenseSlotId;
2127		#adsenseAdType = aiContentGeneration_adsenseAdType;
2128		#adsenseResponsive = aiContentGeneration_adsenseResponsive;
2129		#adsenseCloseButtonDelay = aiContentGeneration_adsenseCloseButtonDelay;
2130		#textLengthOptions = [];
2131		#toneOptions = aiContentGeneration_tones;
2132		#outputStyle = null;
2133
2134		#lastResponseId = null;
2135		#baseResponseId = null;
2136		#lastResponseIndex = 0;
2137		#responseMetadataById = new Map();
2138		#adoptionLoggedActionKeys = new Set();
2139		#editorAdapter = null;
2140		#adManager = null;
2141		#abortController = null;
2142
2143		#modal = null;
2144		#overlay = null;
2145		#modalShadowHost = null;
2146		#modalShadowRoot = null;
2147		#sendChatButton = null;
2148		#helloByAI = null;
2149		#conversationThreads = null;
2150
2151		// オプション
2152		#userRequestTextAreaOption = null;
2153		#textLengthOption = null;
2154		#toneOption = null;
2155
2156		#zIndexBase = 300000;
2157
2158		constructor(contentType) {
2159			this.contentType = contentType;
2160			this.#textLengthOptions = aiContentGeneration_textLengthOptions[contentType] || [];
2161			this.#outputStyle = aiContentGeneration_outputStyles[contentType];
2162		}
2163
2164		/** エディタ汎用アダプタ(遅延生成)。ここだけがエディタ実装依存 */
2165		#helperEditor() {
2166			if (this.#editorAdapter && this.#editorAdapter.type !== 'none') {
2167				return this.#editorAdapter;
2168			}
2169
2170			if (window.FCKeditorAPI) {
2171				this.#editorAdapter = this.#editorFck();
2172			}
2173			else {
2174				this.#editorAdapter = this.#editorNull();
2175			}
2176
2177			return this.#editorAdapter;
2178		}
2179
2180		#editorNull() {
2181			return {
2182				type: 'none',
2183				detectInstanceName() { return null; },
2184				makeInstance() { return null; },
2185				getHtml() { return ""; },
2186				insertHtml() { return false; },
2187				setHtml() { return false; },
2188				installSafetyHooks() {/* noop */ }
2189			};
2190		}
2191
2192		#editorFck() {
2193			return {
2194				type: 'fck',
2195				detectInstanceName(container) {
2196					let ifr = null;
2197					if (container && container.querySelector) {
2198						ifr = container.querySelector('iframe[id$="___Frame"]');
2199					}
2200					if (!ifr) {
2201						ifr = document.querySelector('iframe[id$="___Frame"]');
2202					}
2203					if (ifr && ifr.id) {
2204						const m = ifr.id.match(/^(.+?)___Frame$/);
2205						if (m) return m[1];
2206					}
2207					return null;
2208				},
2209				makeInstance(name) {
2210					if (!name || !window.FCKeditorAPI) return null;
2211					try { return window.FCKeditorAPI.GetInstance(name); }
2212					catch (_) { return null; }
2213				},
2214				getHtml(ed) {
2215					if (!ed) return "";
2216					try { return ed.GetXHTML(true); }
2217					catch (_) { try { return ed.GetHTML(); } catch (_) { return ""; } }
2218				},
2219				insertHtml(ed, html) {
2220					if (!ed) return false;
2221					ed.Focus(); ed.InsertHtml(html); return true;
2222				},
2223				setHtml(ed, html) {
2224					if (!ed) return false;
2225					ed.Focus(); ed.SetHTML(html); return true;
2226				},
2227				installSafetyHooks() {
2228					window.FCKeditor_OnComplete = (function (orig) {
2229						return function (editorInstance) {
2230							if (typeof orig === "function") orig(editorInstance);
2231						};
2232					})(window.FCKeditor_OnComplete);
2233				}
2234			};
2235		}
2236
2237		// ========== モーダル生成(インスタンス内シングルトン) ==========
2238		async #initModal() {
2239			if (this.#modal) {
2240				this.#modal = null;
2241			}
2242
2243			this.#initZIndexBase();
2244
2245			// 既存を削除(重複ID回避)
2246			window.top.document.getElementById('ai_modal')?.remove();
2247			window.top.document.getElementById('ai_modal_overlay')?.remove();
2248			window.top.document.getElementById('ai_modal_host')?.remove();
2249
2250			// shadow host
2251			this.#modalShadowHost = window.top.document.createElement('div');
2252			this.#modalShadowHost.id = 'ai_modal_host';
2253			window.top.document.body.appendChild(this.#modalShadowHost);
2254
2255			// shadow root
2256			this.#modalShadowRoot = this.#modalShadowHost.attachShadow({ mode: 'open' });
2257
2258			// CSS
2259			const style = document.createElement('link');
2260			style.rel = 'stylesheet';
2261			style.href = '/_administrator/css/default/ai_content_generation_modal.css?t=1790968415';
2262
2263			this.#modalShadowRoot.appendChild(style);
2264
2265			await new Promise((resolve, reject) => {
2266				style.onload = () => resolve();
2267				style.onerror = () => reject(new Error('Failed to load CSS'));
2268			});
2269
2270			this.#modalShadowRoot.innerHTML += this.#templateModal();
2271
2272			this.#modal = this.#modalShadowRoot.getElementById('ai_modal');
2273			this.#modal.style.zIndex = this.#zIndexBase + 2;
2274			this.#overlay = this.#modalShadowRoot.getElementById('ai_modal_overlay');
2275			this.#overlay.style.zIndex = this.#zIndexBase + 1;
2276			this.#userRequestTextAreaOption = this.#modal.querySelector('[data-name="ai_modal_options_user_request"]');
2277			this.#textLengthOption = this.#modal.querySelector('[data-name="ai_modal_options_text_length"]');
2278			this.#toneOption = this.#modal.querySelector('[data-name="ai_modal_options_tone"]');
2279			this.#sendChatButton = this.#modal.querySelector('[data-name="ai_modal_send_chat_button"]');
2280			this.#conversationThreads = this.#modal.querySelector('[data-name="ai_modal_conversation_threads"]');
2281			this.#helloByAI = this.#modal.querySelector('[data-name="ai_modal_hello_by_ai"]');
2282
2283			// イベント設定
2284			this.#modal.querySelector('[data-name="ai_modal_close_button"]').addEventListener("click", () => this.#closeModal('close_button'));
2285			this.#modal.querySelector('[data-name="ai_modal_btn_cancel"]').addEventListener("click", () => this.#closeModal('cancel_button'));
2286			this.#setupManualLinks();
2287
2288			if (this.#showAD) {
2289				try {
2290					this.#initAdManager();
2291					this.#adManager.loadAdSdk();
2292				} catch (e) {
2293					console.error('広告SDKの読み込みに失敗しました:', e);
2294				}
2295			}
2296
2297			// チャットの送信
2298			this.#sendChatButton.addEventListener("click", async (event) => {
2299
2300				const targetButton = event.currentTarget;
2301
2302				const isFirstRequest = !this.#lastResponseId;
2303				const previousResponseId = this.#lastResponseId;
2304				const nextResponseIndex = isFirstRequest
2305					? 1
2306					: this.#lastResponseIndex + 1;
2307
2308				const userRequest = this.#userRequestTextAreaOption.value || "";
2309				this.#userRequestTextAreaOption.value = "";
2310				const chatId = this.#addUserChat(userRequest);
2311
2312				targetButton.disabled = true;
2313				this.#userRequestTextAreaOption.disabled = true;
2314
2315				if (this.#showAD && isFirstRequest && this.#adManager) {
2316					this.#adManager.newAdContainer();
2317					this.#adManager.initializeAds();
2318				}
2319
2320				const shownHelloByAI = this.#hideHelloByAI();
2321
2322				this.#hideInputSection();
2323				this.#addLoadingIndicator();
2324				this.#scrollToConversationEnd();
2325
2326				let inProgressChatId = null;
2327				let streamingHtml = '';
2328				let chatShown = false;
2329
2330				try {
2331					inProgressChatId = this.#addAIChat('', `pending_${crypto.randomUUID()}`);
2332					await this.#sendAIRequest(
2333						this.contentType,
2334						userRequest,
2335						this.#textLengthOption.value,
2336						this.#toneOption.value,
2337						isFirstRequest ? this.#getRawTextOfBubbleById() : null,
2338						!isFirstRequest ? previousResponseId : null,
2339						!isFirstRequest ? this.#baseResponseId : null,
2340						nextResponseIndex,
2341						// onDelta
2342						(delta) => {
2343							streamingHtml += delta;
2344							this.#setAIChatContent(inProgressChatId, streamingHtml);
2345							this.#scrollToConversationEnd();
2346							if (!chatShown) {
2347								this.#readyToNewChat(inProgressChatId);
2348								chatShown = true;
2349							}
2350						},
2351						// onDone
2352						(fullText, responseId, baseResponseId, responseIndex) => {
2353							this.#setAIChatContent(inProgressChatId, fullText);
2354							inProgressChatId = this.#finalizeAIChatResponseId(inProgressChatId, responseId);
2355							this.#lastResponseId = responseId;
2356							this.#baseResponseId = baseResponseId || responseId;
2357							this.#lastResponseIndex = Number(responseIndex) || nextResponseIndex;
2358							this.#responseMetadataById.set(responseId, {
2359								previousResponseId,
2360								baseResponseId: this.#baseResponseId,
2361								responseIndex: this.#lastResponseIndex,
2362							});
2363							if (!isFirstRequest) {
2364								this.#sendAdoptionAction('regenerated', 'new_generation', previousResponseId);
2365							}
2366							this.#readyToNewChat(inProgressChatId);
2367							this.#showAIChatActions(inProgressChatId);
2368						},
2369						// onError
2370						(errorMessage) => {
2371							throw {
2372								message: errorMessage,
2373								alertToUser: true,
2374							};
2375						},
2376					);
2377
2378				} catch (error) {
2379					// リクエストが中断された場合は何もしない
2380					if (error.name === 'AbortError') {
2381						return;
2382					}
2383
2384					this.#userRequestTextAreaOption.value = userRequest;
2385					this.#removeUserChat(chatId);
2386					if (inProgressChatId) {
2387						this.#removeAIChat(inProgressChatId);
2388					}
2389					if (shownHelloByAI) {
2390						this.#showHelloByAI();
2391					}
2392					this.#removeLoadingIndicator();
2393					this.#adManager?.removeAdContainer();
2394					let errorMessage = error.alertToUser
2395						? error.message
2396						: '予期せぬエラーが発生しました。';
2397
2398					const brCount = (errorMessage.match(/<br>/g) || []).length;
2399
2400					this.#hideAIModal();
2401					window.top.alert_dialog(errorMessage, 110 + (brCount * 20), () => {
2402						this.#showAIModal();
2403						return true;
2404					});
2405
2406					this.#showInputSection();
2407
2408				} finally {
2409					targetButton.disabled = false;
2410					this.#userRequestTextAreaOption.disabled = false;
2411				}
2412			});
2413		}
2414
2415		#initZIndexBase() {
2416			this.#zIndexBase = window.top.document.getElementById('overlay_modal')?.style.zIndex || 300000;
2417		}
2418
2419		#initValues(ctx) {
2420			let targetText = "";
2421			if (ctx.ce instanceof HTMLInputElement || ctx.ce instanceof HTMLTextAreaElement) {
2422				targetText = ctx.ce.value || "";
2423			}
2424			else if (ctx && ctx.editor) {
2425				targetText = this.#helperEditor().getHtml(ctx.editor);
2426			}
2427			else if (ctx && ctx.ce) {
2428				targetText = ctx.ce.innerHTML;
2429			}
2430			else {
2431				targetText = "";
2432			}
2433
2434			window.__ai_modal_ctx__ = ctx;
2435
2436			// オプションの値をリセット
2437			if (this.#userRequestTextAreaOption) {
2438				this.#userRequestTextAreaOption.value = "";
2439			}
2440			if (this.#textLengthOption) {
2441				this.#textLengthOption.value = "";
2442			}
2443			if (this.#toneOption) {
2444				this.#toneOption.value = "";
2445			}
2446			this.#lastResponseId = null;
2447			this.#baseResponseId = null;
2448			this.#lastResponseIndex = 0;
2449			this.#responseMetadataById.clear();
2450			this.#adoptionLoggedActionKeys.clear();
2451			this.#initConversation();
2452
2453			if (/^<br\s*\/?>$/i.test(targetText)) {
2454				targetText = "";
2455			}
2456
2457			this.#addAIChat(targetText);
2458			if (!targetText) {
2459				this.#conversationThreads.querySelector('[data-name="ai_chat_init"]').classList.add('hidden');
2460				this.#showHelloByAI();
2461				this.#userRequestTextAreaOption.setAttribute('placeholder', this.#userRequestTextAreaOption.dataset.placeholderForEmpty || '');
2462			} else {
2463				this.#hideHelloByAI();
2464			}
2465
2466			this.#initTextLengthOption(targetText);
2467			this.#initToneOption();
2468		}
2469
2470		#initTextLengthOption(targetText) {
2471			this.#textLengthOptions.forEach(optionValue => {
2472				this.#textLengthOption.insertAdjacentHTML('beforeend', `
2473					<option value="${_h(optionValue)}">${_h(optionValue)}文字以下</option>
2474				`);
2475			});
2476
2477			if (targetText.length > 0) {
2478				this.#textLengthOption.insertAdjacentHTML('beforeend', `
2479					<option value="keep">既存の文字数を目安に</option>
2480				`);
2481			}
2482		}
2483
2484		#initToneOption() {
2485			this.#toneOptions.forEach(tone => {
2486				this.#toneOption.insertAdjacentHTML('beforeend', `
2487					<option value="${_h(tone)}">${_h(tone)}</option>
2488				`);
2489			});
2490		}
2491
2492		#readyToNewChat(aiChatId) {
2493			this.#removeLoadingIndicator();
2494			this.#showAIChat(aiChatId);
2495			this.#showInputSection();
2496		}
2497
2498		#showHelloByAI() {
2499			this.#helloByAI.classList.remove("hidden");
2500		}
2501
2502		#hideHelloByAI() {
2503			let changed = false;
2504			if (!this.#helloByAI.classList.contains("hidden")) {
2505				this.#helloByAI.classList.add("hidden");
2506				changed = true;
2507			}
2508			return changed;
2509		}
2510
2511		async #openModalWithContext(ctx) {
2512			await this.#initModal();
2513
2514			this.#initValues(ctx);
2515
2516			this.#showAIModal();
2517
2518			this.#userRequestTextAreaOption.focus();
2519		}
2520
2521		// ========== ボタンにモーダルをバインド ==========
2522		bindAiModal(buttonEl, ce = null) {
2523			buttonEl.addEventListener("click", () => {
2524				let instanceName = ce === null
2525					? this.#helperEditor().detectInstanceName(document)
2526					: null;
2527				let editor = null;
2528				if (instanceName) {
2529					editor = this.#helperEditor().makeInstance(instanceName);
2530				}
2531
2532				if (ce === null) {
2533					if (document.activeElement &&
2534						document.activeElement.hasAttribute &&
2535						document.activeElement.hasAttribute("contenteditable")) {
2536						ce = document.activeElement;
2537					}
2538				}
2539
2540				if (!editor && !ce) {
2541					instanceName = this.#helperEditor().detectInstanceName(document);
2542					if (instanceName) {
2543						editor = this.#helperEditor().makeInstance(instanceName);
2544					}
2545					if (!ce) {
2546						ce = document.querySelector('[contenteditable="true"]');
2547					}
2548				}
2549
2550				this.#openModalWithContext({ editor, ce });
2551			});
2552
2553			// エディタのセット
2554			this.#helperEditor().installSafetyHooks();
2555		}
2556
2557		#closeModal(actionSource = null) {
2558			if (actionSource) {
2559				this.#sendAdoptionAction('dismissed', actionSource);
2560			}
2561			// 進行中のフェッチリクエストを中断
2562			this.#abortController?.abort();
2563			this.#abortController = null;
2564
2565			this.#adManager?.removeAdContainer();
2566			this.#initConversation();
2567			this.#hideAIModal();
2568			this.#modal?.remove();
2569			this.#modal = null;
2570			this.#overlay?.remove();
2571			this.#overlay = null;
2572			this.#modalShadowHost?.remove();
2573			this.#modalShadowHost = null;
2574			this.#modalShadowRoot = null;
2575		}
2576
2577		#setTextAndCloseModal(responseId) {
2578			const ctx = window.__ai_modal_ctx__;
2579			if (!ctx) return;
2580
2581			const text = this.#getRawTextOfBubbleById(responseId);
2582
2583			if (ctx.editor) {
2584				if (this.#helperEditor().setHtml(ctx.editor, text)) {
2585					this.#sendAdoptionAction('applied', 'decision_button', responseId);
2586					this.#closeModal();
2587				}
2588				else {
2589					throw new Error('エディタの準備ができていません。');
2590				}
2591			}
2592			else if (ctx.ce) {
2593				// NOTE: 必要に応じて sanitize を検討
2594				if (ctx.ce instanceof HTMLInputElement || ctx.ce instanceof HTMLTextAreaElement) {
2595					ctx.ce.value = text;
2596				} else {
2597					ctx.ce.innerHTML = text;
2598				}
2599				ctx.ce.dispatchEvent(new Event('change', { bubbles: true }));
2600				this.#sendAdoptionAction('applied', 'decision_button', responseId);
2601				this.#closeModal();
2602			}
2603		}
2604
2605		#initConversation() {
2606			this.#conversationThreads.replaceChildren();
2607		}
2608
2609		#addUserChat(chatByUser) {
2610			const chatId = `user_chat_${crypto.randomUUID()}`;
2611			this.#conversationThreads.insertAdjacentHTML('beforeend', this.#templateUserChat(chatId, chatByUser));
2612			return chatId;
2613		}
2614
2615		#removeUserChat(chatId) {
2616			this.#conversationThreads.querySelector(`[data-name="${chatId}"]`).remove();
2617		}
2618
2619		#htmlToPlainText(html) {
2620			const template = document.createElement('template');
2621			html = html.replace(/<br\s*\/?>\n?/gi, '\n');
2622			template.innerHTML = html;
2623			return template.content.textContent ?? '';
2624		}
2625
2626		#setAIChatContent(chatId, chatByAI = '') {
2627			if (!chatId) {
2628				return;
2629			}
2630			if (!this.#conversationThreads) {
2631				return;
2632			}
2633			const thread = this.#conversationThreads.querySelector(`[data-name="${chatId}"]`);
2634			if (!thread) {
2635				return;
2636			}
2637			const chatBubbleByAI = thread.querySelector('[data-name="chatBubbleByAI"]');
2638			const rawChatByAI = thread.querySelector('[data-name="rawChatByAI"]');
2639			if (!chatBubbleByAI || !rawChatByAI) {
2640				return;
2641			}
2642			rawChatByAI.textContent = chatByAI ?? '';
2643			if (chatBubbleByAI.dataset.showingSource === "true") {
2644				chatBubbleByAI.textContent = rawChatByAI.textContent;
2645			} else {
2646				chatBubbleByAI.innerText = this.#htmlToPlainText(chatByAI ?? '');
2647			}
2648		}
2649
2650		#finalizeAIChatResponseId(chatId, responseId) {
2651			const thread = this.#conversationThreads.querySelector(`[data-name="${chatId}"]`);
2652			if (!thread) {
2653				return chatId;
2654			}
2655			const newChatId = `ai_chat_${responseId}`;
2656			thread.dataset.name = newChatId;
2657			const adoptButton = thread.querySelector('[data-action="adopt"]');
2658			if (adoptButton) {
2659				adoptButton.dataset.responseId = responseId;
2660				adoptButton.disabled = false;
2661			}
2662			return newChatId;
2663		}
2664
2665		#removeAIChat(chatId) {
2666			if (!chatId) {
2667				return;
2668			}
2669			if (!this.#conversationThreads) {
2670				return;
2671			}
2672			this.#conversationThreads.querySelector(`[data-name="${chatId}"]`)?.remove();
2673		}
2674
2675		#showAIChat(chatId) {
2676			this.#conversationThreads.querySelector(`[data-name="${chatId}"]`).classList.remove('hidden');
2677		}
2678
2679		#addAIChat(chatByAI, responseId = null) {
2680
2681			const isInitial = responseId === null;
2682
2683			const chatId = isInitial
2684				? `ai_chat_init`
2685				: `ai_chat_${responseId}`;
2686
2687			this.#conversationThreads.insertAdjacentHTML('beforeend', this.#templateAIChat(chatId));
2688
2689			const newThread = this.#conversationThreads.querySelector(`[data-name="${chatId}"]`);
2690			if (!isInitial) {
2691				newThread.classList.add('hidden');
2692				this.#hideAIChatActions(chatId);
2693			}
2694
2695			this.#setAIChatContent(chatId, chatByAI ?? '');
2696
2697			// ソース表示トグル
2698			const sourceToggle = newThread.querySelector('[data-action="showSource"]');
2699			if (this.#outputStyle === 'html') {
2700				sourceToggle.addEventListener("click", (event) => {
2701					const btn = event.currentTarget;
2702					const isActive = event.currentTarget.dataset.status === "true";
2703					const chatBubbleByAI = newThread.querySelector('[data-name="chatBubbleByAI"]');
2704					const rawChatByAI = newThread.querySelector('[data-name="rawChatByAI"]');
2705					if (isActive) {
2706						chatBubbleByAI.innerText = this.#htmlToPlainText(rawChatByAI.textContent);
2707						chatBubbleByAI.dataset.showingSource = "false";
2708						btn.classList.remove('cms-btn-ai-source--active');
2709					} else {
2710						chatBubbleByAI.textContent = rawChatByAI.textContent;
2711						chatBubbleByAI.dataset.showingSource = "true";
2712						btn.classList.add('cms-btn-ai-source--active');
2713					}
2714					event.currentTarget.dataset.status = isActive ? "false" : "true";
2715				});
2716			}
2717			else {
2718				sourceToggle.remove();
2719			}
2720
2721			// 決定ボタン
2722			const adoptButton = newThread.querySelector('button[data-action="adopt"]');
2723			adoptButton.addEventListener("click", () => {
2724				const responseId = adoptButton.dataset.responseId || null;
2725				this.#setTextAndCloseModal(responseId);
2726			});
2727			if (isInitial) {
2728				adoptButton.remove();
2729			}
2730
2731			// 初回以外は「現在の表示内容」ラベルを削除
2732			const chatLabel = newThread.querySelector('[data-name="chatLabel"]');
2733			if (!isInitial) {
2734				chatLabel.remove();
2735			}
2736
2737			return chatId;
2738		}
2739
2740		#hideAIChatActions(chatId) {
2741			const thread = this.#conversationThreads.querySelector(`[data-name="${chatId}"]`);
2742			if (!thread) {
2743				return;
2744			}
2745			const actions = thread.querySelector('[data-name="actions"]');
2746			if (actions) {
2747				actions.classList.add('hidden');
2748			}
2749		}
2750
2751		#showAIChatActions(chatId) {
2752			const thread = this.#conversationThreads.querySelector(`[data-name="${chatId}"]`);
2753			if (!thread) {
2754				return;
2755			}
2756			const actions = thread.querySelector('[data-name="actions"]');
2757			if (actions) {
2758				actions.classList.remove('hidden');
2759			}
2760		}
2761
2762		#getRawTextOfBubbleById(responseId = null) {
2763			const id = responseId === null
2764				? `ai_chat_init`
2765				: `ai_chat_${responseId}`;
2766
2767			const rawChatByAI = this.#conversationThreads.querySelector(`[data-name="${id}"]`).querySelector('[data-name="rawChatByAI"]');
2768			return rawChatByAI.textContent;
2769		}
2770
2771		#addLoadingIndicator() {
2772			this.#conversationThreads.insertAdjacentHTML('beforeend', this.#templateLoadingIndicator());
2773		}
2774
2775		#removeLoadingIndicator() {
2776			this.#conversationThreads.querySelectorAll('[data-name="ai_content_generation_loading_indicator"]').forEach(el => el.remove());
2777		}
2778
2779		async #sendAIRequest(
2780			content_type,
2781			user_request,
2782			text_length,
2783			tone,
2784			current_input = null,
2785			previous_response_id = null,
2786			base_response_id = null,
2787			response_index = 1,
2788			onDelta,
2789			onDone,
2790			onError,
2791		) {
2792			// 既存の進行中のリクエストがあれば中断
2793			this.#abortController?.abort();
2794
2795			const request_end_point = 'ai_content_generation_manager/generate_content';
2796
2797			const form = document.createElement('form');
2798			form.method = 'POST';
2799
2800			const createHidden = (name, value) => {
2801				const input = document.createElement('input');
2802				input.type = 'hidden';
2803				input.name = name;
2804				input.value = value;
2805				return input;
2806			};
2807
2808			const fields = [
2809				{ name: 'q', value: request_end_point },
2810				{ name: 'do', value: request_end_point },
2811			];
2812
2813			fields.push({ name: 'content_type', value: content_type });
2814			fields.push({ name: 'user_request', value: user_request });
2815			fields.push({ name: 'text_length', value: text_length });
2816			fields.push({ name: 'tone', value: tone });
2817			if (current_input !== null) {
2818				fields.push({ name: 'current_input', value: current_input });
2819			}
2820			if (previous_response_id !== null) {
2821				fields.push({ name: 'previous_response_id', value: previous_response_id });
2822			}
2823			if (base_response_id !== null) {
2824				fields.push({ name: 'base_response_id', value: base_response_id });
2825			}
2826			fields.push({ name: 'response_index', value: response_index });
2827
2828			fields.forEach(({ name, value }) => form.appendChild(createHidden(name, value)));
2829
2830			const formData = new FormData(form);
2831
2832			const headers = new Headers({
2833				'X-Requested-With': 'XMLHttpRequest',
2834				'Accept': 'text/event-stream',
2835			});
2836
2837			// AbortControllerを作成してリクエストを中断可能にする
2838			this.#abortController = new AbortController();
2839
2840			const res = await fetch("/_administrator/connector.php", {
2841				method: 'POST',
2842				body: formData,
2843				credentials: 'same-origin',
2844				signal: this.#abortController.signal,
2845				headers,
2846			});
2847			if (!res.ok) {
2848				throw new Error(`HTTP ${res.status}`);
2849			}
2850
2851			const stream = res.body;
2852			const decoder = new TextDecoder();
2853
2854			let buffer = '';
2855			let responseId = null;
2856
2857			const processPayload = (payload) => {
2858				if (!payload) {
2859					return;
2860				}
2861
2862				let parsed;
2863				try {
2864					parsed = JSON.parse(payload);
2865				} catch (_) {
2866					return;
2867				}
2868
2869				if (parsed.type === 'delta') {
2870					const deltaText = parsed.text;
2871					onDelta(deltaText);
2872				}
2873				else if (parsed.type === 'done') {
2874					const finalText = parsed.fullText;
2875					responseId = parsed.responseId;
2876					onDone(finalText, responseId, parsed.baseResponseId, parsed.responseIndex);
2877				}
2878				else if (parsed.type === 'error') {
2879					onError(parsed.message);
2880				}
2881			};
2882
2883			const extractDataPayload = (rawEvent) => {
2884				const payloadLines = [];
2885				rawEvent.split(/\r?\n/).forEach(line => {
2886					if (line.startsWith('data:')) {
2887						payloadLines.push(line.slice(5).trimStart());
2888					}
2889				});
2890				return payloadLines.join('\n').trim();
2891			};
2892
2893			const flushBuffer = () => {
2894				let separatorIndex;
2895				while ((separatorIndex = buffer.indexOf('\n\n')) !== -1) {
2896					const rawEvent = buffer.slice(0, separatorIndex).trim();
2897					buffer = buffer.slice(separatorIndex + 2);
2898					const payload = extractDataPayload(rawEvent);
2899					if (payload) {
2900						processPayload(payload);
2901					}
2902				}
2903			};
2904
2905			for await (const chunk of stream) {
2906				buffer += decoder.decode(chunk, { stream: true });
2907				flushBuffer();
2908			}
2909		}
2910
2911		#sendAdoptionAction(action, actionSource, responseId = this.#lastResponseId) {
2912			const responseMetadata = this.#responseMetadataById.get(responseId);
2913			if (!responseId || !responseMetadata || !responseMetadata.baseResponseId || responseMetadata.responseIndex < 1) {
2914				return;
2915			}
2916			const adoptionLogKey = `${responseId}:${action}`;
2917			if (this.#adoptionLoggedActionKeys.has(adoptionLogKey)) {
2918				return;
2919			}
2920			this.#adoptionLoggedActionKeys.add(adoptionLogKey);
2921
2922			const requestEndPoint = 'ai_content_generation_manager/log_adoption_action';
2923			const params = new URLSearchParams();
2924			params.append('q', requestEndPoint);
2925			params.append('do', requestEndPoint);
2926			params.append('content_type', this.contentType);
2927			params.append('response_id', responseId);
2928			params.append('base_response_id', responseMetadata.baseResponseId);
2929			params.append('response_index', String(responseMetadata.responseIndex));
2930			params.append('action', action);
2931			params.append('action_source', actionSource || '');
2932			if (responseMetadata.previousResponseId !== null) {
2933				params.append('previous_response_id', responseMetadata.previousResponseId);
2934			}
2935
2936			fetch('/_administrator/connector.php', {
2937				method: 'POST',
2938				body: params.toString(),
2939				credentials: 'same-origin',
2940				keepalive: true,
2941				headers: new Headers({
2942					'X-Requested-With': 'XMLHttpRequest',
2943					'Accept': 'application/json',
2944					'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
2945				}),
2946			}).catch(() => {});
2947		}
2948
2949		#initAdManager() {
2950			if (this.#adManager) {
2951				return;
2952			}
2953			this.#adManager = new globalThis.AiAdManager({
2954				adProvider: this.#adProvider,
2955				adVastTag: this.#adVastTag,
2956				adsenseClientId: this.#adsenseClientId,
2957				adsenseSlotId: this.#adsenseSlotId,
2958				adsenseAdType: this.#adsenseAdType,
2959				adsenseResponsive: this.#adsenseResponsive,
2960				adsenseCloseButtonDelay: this.#adsenseCloseButtonDelay,
2961			});
2962			this.#adManager.setZIndexBase(this.#zIndexBase);
2963		}
2964
2965		#scrollToConversationEnd() {
2966			this.#conversationThreads.lastElementChild?.scrollIntoView({ behavior: 'smooth' });
2967		}
2968
2969		#hideInputSection = () => {
2970			this.#modal.querySelector('[data-name="ai_modal_input_section"]').classList.add("hidden");
2971		}
2972
2973		#showInputSection = () => {
2974			this.#modal.querySelector('[data-name="ai_modal_input_section"]').classList.remove("hidden");
2975		}
2976
2977		#hideAIModal = () => {
2978			this.#modal.classList.add("hidden");
2979			this.#overlay.classList.add("hidden");
2980		}
2981
2982		#showAIModal = () => {
2983			this.#modal.classList.remove("hidden");
2984			this.#overlay.classList.remove("hidden");
2985		}
2986
2987		#setupManualLinks = () => {
2988			const movieBtn = this.#modal.querySelector('.cms-dialog-top-movie');
2989			fetch(aiContentGeneration_videoManualApiURL)
2990				.then((res) => (res.ok ? res.text() : ''))
2991				.then((movieId) => {
2992					if (!movieId) return;
2993					movieBtn.addEventListener("click", () => OpenSubWin('https://www.youtube.com/embed/' + _uc(movieId) + '/?rel=0', 920, 520, 'movie_manual'));
2994					movieBtn.hidden = false;
2995				})
2996				.catch(() => {});
2997			const manualBtn = this.#modal.querySelector('.cms-dialog-top-help');
2998			fetch(aiContentGeneration_helpCategoryApiURL)
2999				.then((res) => (res.ok ? res.text() : ''))
3000				.then((helpPath) => {
3001					if (!helpPath) return;
3002					manualBtn.addEventListener("click", () => OpenSubWin(aiContentGeneration_helpPageBaseURL + _us(helpPath), 760, 860, 'help'));
3003					manualBtn.hidden = false;
3004				})
3005				.catch(() => {});
3006		}
3007
3008		#templateModal = () => {
3009			let adminName = _h(this.#adminName);
3010			return `
3011				<div id="ai_modal_overlay" class="cms-ai-overlay"></div>
3012				<div id="ai_modal" class="cms-ai-modal">
3013						<div class="cms-ai-modal-header">
3014							<div class="cms-ai-modal-header__title">
3015								<div class="cms-ai-modal-header__title-text">ゴーゴーAIで生成</div>
3016								<div class="cms-ai-badge">
3017									<div class="cms-ai-badge__icon">
3018										<img src="/_administrator/css/default/images/ai/logo-gogoai.svg" alt="AI">
3019									</div>
3020									<div class="cms-ai-badge__body">
3021										<div class="cms-ai-badge__text">BETA</div>
3022									</div>
3023								</div>
3024							</div>
3025							<div class="cms-ai-modal-header__btn">
3026								<span class="cms-helpsite-btn cms-dialog-top-movie" hidden>
3027									<svg>
3028										<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-movie-manual"/>
3029									</svg>
3030								</span>
3031								<span class="cms-helpsite-btn cms-dialog-top-help" hidden>
3032									<svg>
3033										<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-help"/>
3034									</svg>
3035								</span>
3036								<a class="cms-dialog-top-close" data-name="ai_modal_close_button">
3037									<svg>
3038										<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-close"/>
3039									</svg>
3040								</a>
3041							</div>
3042						</div>
3043						<div class="cms-ai-modal-body">
3044							<div data-name="ai_modal_hello_by_ai" class="cms-ai-hello">
3045								こんにちは、${adminName}さん<br>
3046								お手伝いできることはありますか?
3047							</div>
3048
3049							<div>
3050								<!-- div style="flex-grow: 1;
3050 overflow-y: scroll;" -->
3051								<div data-name="ai_modal_conversation_threads"
3052									style="
3053										display: flex;
3054										flex-direction: column;
3055										gap: 12px;
3056									"
3057								>
3058								</div>
3059							</div>
3060
3061							<div>
3062								<div
3063									data-name="ai_modal_input_section" class="cms-chat-input-section">
3064									<textarea data-name="ai_modal_options_user_request" class="cms-chat-user-request"
3065										rows="2"
3066										placeholder="ゴーゴーAIへのリクエストを入力してみよう!\nトーンと文字数を選ぶだけでもOK!"
3067										data-placeholder-for-empty="ゴーゴーAIへのリクエストを入力してみよう!"
3068									></textarea>
3069
3070									<div class="cms-chat-input-footer">
3071										<div class="cms-chat-input-footer__option">
3072											<div class="cms-chat-select">
3073												<select data-name="ai_modal_options_tone" class="cms-chat-select-option">
3074													<option value="" selected>トーンを指定しない</option>
3075												</select>
3076											</div>
3077											<div class="cms-chat-select">
3078												<select data-name="ai_modal_options_text_length" class="cms-chat-select-option">
3079													<option value="" selected>文字数を指定しない</option>
3080												</select>
3081											</div>
3082										</div>
3083										<div class="cms-chat-input-footer__send">
3084											<button type="button" data-name="ai_modal_send_chat_button" class="cms-btn-chat-send">
3085												<svg>
3086													<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-edit"></use>
3087												</svg>
3088												作成
3089											</button>
3090										</div>
3091									</div>
3092								</div>
3093							</div>
3094						</div>
3095						<div class="cms-ai-modal-footer">
3096							<div class="cms-ai-modal-footer__info">
3097								※AIで生成されたテキストです。文字数は概算であり、内容に誤りがある可能性がありますので、ご確認の上ご利用ください。
3098							</div>
3099							<button type="button" data-name="ai_modal_btn_cancel" class="cms-ai-btn cms-ai-btn--cancel">
3100								<svg>
3101									<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-cancel"/>
3102								</svg>
3103								キャンセル
3104							</button>
3105						</div>
3106				</div>
3107			`;
3108		}
3109
3110		#templateUserChat = (chatId, chatByUser) => {
3111			chatId = _h(chatId);
3112			chatByUser = _h(chatByUser);
3113
3114			const tone = this.#toneOption.value || null;
3115			const textLength = this.#textLengthOption.value || null;
3116			let toneLabel = '';
3117			if (tone !== null && tone !== '') {
3118				toneLabel = `トーン「${tone}」`;
3119			}
3120
3121			let textLengthLabel = '';
3122			if (textLength !== null && textLength !== '') {
3123				textLengthLabel = this.#textLengthOption.querySelector('option[value="' + textLength + '"]').textContent;
3124				textLengthLabel = `文字数「${textLengthLabel}」`;
3125			}
3126			const optionsText = toneLabel + textLengthLabel;
3127
3128			if (!chatByUser) {
3129				chatByUser = `<small><i>${optionsText ? optionsText + 'で' : ''}ゴーゴーAIにおまかせ</i></small>`;
3130			} else if (optionsText) {
3131				chatByUser += `<br><small><i>${optionsText}</i></small>`;
3132			}
3133			// 改行を<br>に変換
3134			chatByUser = chatByUser.replace(/\n/g, '<br>');
3135			return `
3136				<div data-name="${chatId}" class="cms-ai-message-list cms-ai-message-list--end">
3137					<div data-name="chatBubbleByUser" class="cms-ai-message cms-ai-message--me">
3138						${chatByUser}
3139					</div>
3140				</div>
3141			`;
3142		}
3143
3144		#templateAIChat = (chatId) => {
3145			chatId = _h(chatId);
3146			return `
3147				<div data-name="${chatId}" class="cms-ai-message-list">
3148					<div data-name="chatLabel"class="cms-ai-message-list__label">現在の表示内容</div>
3149					<div data-name="chatBubbleByAI" data-showing-source="false" class="cms-ai-message cms-ai-message--ai">
3150					</div>
3151					<div class="cms-btn-ai-group" data-name="actions">
3152						<button type="button" class="cms-btn-ai-source" data-action="showSource" data-status="false">
3153							<svg>
3154								<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-source"/>
3155							</svg>
3156							ソース
3157						</button>
3158						<button type="button" class="cms-btn-ai-decision" data-action="adopt">
3159							<svg>
3160								<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-ok"/>
3161							</svg>
3162							上記で決定する
3163						</button>
3164					</div>
3165
3166					<div data-name="rawChatByAI" style="
3167						display: none;
3168					">
3169					</div>
3170				</div>
3171			`;
3172		}
3173
3174		#templateLoadingIndicator = () => {
3175			return `
3176				<div data-name="ai_content_generation_loading_indicator">
3177					<div style="
3178						display: flex;
3179						align-items: center;
3180						gap: 6px;
3181					">
3182						<div class="cms-ai-loading-icon-pulse"></div>
3183						
3184						<div
3185							style="
3186								background: linear-gradient(-45deg,#fff 0%,#ccc 25%,#666 50%,#ccc 75%,#fff 100%);
3187								background-size: 200% auto;
3188								-webkit-background-clip: text;
3189								background-clip: text;
3190								color: transparent;
3191								animation: ai_modal_text_on_loading_shine 5s linear infinite;
3192							">
3193							ゴーゴーAIテキストを生成中...
3194						</div>
3195						<style>
3196							@keyframes ai_modal_text_on_loading_shine {
3197								0%   { background-position: 300% center; }
3198								100% { background-position: -300% center; }
3199							}
3200						</style>
3201					</div>
3202				</div>
3203			`;
3204		}
3205
3206	}
3207
3208}
3209
3210
3211if (typeof globalThis.aiTextChecker !== 'function') {
3212
3213	const aiTextChecker_checkTypes = {"typo":{"label":"誤字脱字チェック","description":"タイプミス、送り仮名の誤り、同音異義語の誤用(「保障」と「保証」など)、全角半角の混在などをチェックします。","default":true},"expression":{"label":"日本語表現チェック","description":"文法の誤り(ら抜き言葉、主述のねじれなど)、敬語の誤用(二重敬語、「させていただく」の濫用など)、二重否定や冗長表現をチェックします。","default":true},"consistency":{"label":"表記ゆれチェック","description":"同じ語がページ内で異なる表記になっている箇所(「サーバー」と「サーバ」のような長音記号の不統一、カタカナ/漢字/ひらがなの不統一など)をチェックします。","default":true},"legal":{"label":"景表法観点チェック","description":"優良誤認・有利誤認・ステルスマーケティングなど、広告表現として確認が必要な可能性のある記述をチェックします。","default":false}};
3214
3215	const aiTextChecker_checkTypeLabels = Object.assign({}, {"typo":"誤字脱字チェック","expression":"日本語表現チェック","consistency":"表記ゆれチェック","legal":"景表法観点チェック"}, { custom: 'カスタムチェック' });
3216
3217	const aiTextChecker_manualId = 'ゴーゴーAIテキストチェック';
3218	const aiTextChecker_helpCategoryApiURL = 'https\x3a\x2f\x2fmanual\x2dv3\x2esecure\x2dcms\x2enet\x2fcategory\x2ephp\x3fc\x3d' + encodeURIComponent(aiTextChecker_manualId);
3219	const aiTextChecker_videoManualApiURL = 'https\x3a\x2f\x2fmanual\x2dv3\x2esecure\x2dcms\x2enet\x2fvideomanual\x2ephp\x3fc\x3d' + encodeURIComponent(aiTextChecker_manualId);
3220	const aiTextChecker_helpPageBaseURL = '\x2f\x5fadministrator\x2feditor\x2fhelp';
3221
3222	globalThis.aiTextChecker = class aiTextChecker {
3223		#pageElementCollector = null;
3224		#checkApiClient = null;
3225		#issueListRenderer = null;
3226		#issueOverlayController = null;
3227		constructor(){
3228			this.#initializeServices();
3229		}
3230
3231		// 現在開いているインスタンス。フローティングボタンは毎回 new するため、再オープン時に前インスタンスの
3232		// 後始末(チップ/スポットライト/グローバルリスナー/タイマ)を確実に通すための世代管理に使う。
3233		static #activeInstance = null;
3234
3235		#modal = null;
3236		#overlay = null;
3237		#shadowHost = null;
3238		#shadowRoot = null;
3239		#modalCssPath = '/_administrator/css/default/ai_text_checker_modal.css';
3240		#globalCssPath = '/_administrator/css/default/ai_text_checker_global.css';
3241		#elementMap = null; // 採番ID と 実DOM要素 と 抽出テキスト の対応
3242		#selectionWait = null; // #waitForCheckSelection の Promise 解決用(×で閉じたとき null を返す)
3243
3244		#initializeServices() {
3245			this.#pageElementCollector = new pageElementCollector();
3246			this.#checkApiClient = new checkApiClient();
3247			this.#issueOverlayController = new issueOverlayController({
3248				doc: window.top.document,
3249				pageElementCollector: this.#pageElementCollector,
3250				modalAccessor: {
3251					isMinimized: () => !!(this.#modal && this.#modal.classList.contains('aiTextCheckerModalMinimized')),
3252					expand: () => {
3253						if (this.#modal && this.#modal.classList.contains('aiTextCheckerModalMinimized')) {
3254							this.#toggleMinimize();
3255						}
3256					},
3257					focusCard: (idx) => {
3258						if (!this.#shadowRoot) return;
3259						const cards = this.#shadowRoot.querySelectorAll('.aiTextCheckerIssueList > li');
3260						const card = cards[idx];
3261						if (!card) return;
3262						card.scrollIntoView({ behavior: 'smooth', block: 'center' });
3263						card.classList.add('aiTextCheckerIssueCardFocused');
3264						setTimeout(() => card.classList.remove('aiTextCheckerIssueCardFocused'), 1500);
3265					},
3266				},
3267			});
3268		}
3269
3270		#initializeViewServices() {
3271			this.#issueListRenderer = new issueListRenderer({
3272				shadowRoot: this.#shadowRoot,
3273				checkTypeLabels: aiTextChecker_checkTypeLabels,
3274				onPanelModeRequired: () => this.#enterPanelMode(),
3275				onJumpToElement: (elementId, idx) => {
3276					const el = this.#pageElementCollector.resolve(this.#elementMap, elementId);
3277					if (el) this.#issueOverlayController.jumpToElement(el, idx);
3278				},
3279				onResolvedChange: (idx, reviewed) => {
3280					this.#issueOverlayController.setChipReviewed(idx, reviewed);
3281				},
3282			});
3283		}
3284
3285		// ========== 公開 API ==========
3286
3287		async open() {
3288			// 現在開いているインスタンスがあれば、その後始末を通して確実に世代交代する。
3289			this.#takeOverActiveInstance();
3290
3291			// モーダルを初期化し、表示用サービスを構築する。
3292			await this.#setupModal();
3293
3294			// チェック項目選択 → 実行 → エラー時は alert_dialog の OK 待ち → 再選択 のループ。
3295			// エラー時はフォーム DOM を破棄せずローディング表示だけ重ね/解除しているため、入力(チェック種別・カスタムプロンプト)は自動的に保持される。
3296			while (true) {
3297				const selection = await this.#waitForCheckSelection();
3298				if (!selection) return;
3299
3300				const errorMessage = await this.#runCheckAndShowResults(selection);
3301				if (errorMessage === null) return;
3302				await this.#showError(errorMessage);
3303			}
3304		}
3305
3306		// 現在「結果表示中の」インスタンスが存在するか
3307		static hasActiveResults() {
3308			return !!(aiTextChecker.#activeInstance && aiTextChecker.#activeInstance.#hasResults());
3309		}
3310
3311		// フローティングボタン等、外部からの起動口。結果表示中の上書き起動は確認を挟む。
3312		static openWithConfirm() {
3313			if (aiTextChecker.hasActiveResults()) {
3314				const inst = aiTextChecker.#activeInstance;
3315				if (inst.#confirmDiscardResults('表示中のチェック結果は失われますが、よろしいですか?', () => new aiTextChecker().open())) {
3316					return;
3317				}
3318			}
3319			new aiTextChecker().open();
3320		}
3321
3322		// ========== メインフロー ==========
3323
3324		#takeOverActiveInstance() {
3325			// 前回開いたインスタンスが閉じられていなければ、その後始末を通してから世代交代する。
3326			// (これをしないと top document 上のチップ/スポットライトや scroll/resize リスナーが残留・蓄積する)
3327			if (aiTextChecker.#activeInstance && aiTextChecker.#activeInstance !== this) {
3328				aiTextChecker.#activeInstance.#closeModal();
3329			}
3330			aiTextChecker.#activeInstance = this;
3331		}
3332
3333		async #setupModal() {
3334			await this.#initModal();
3335			this.#initializeViewServices();
3336		}
3337
3338		#waitForCheckSelection() {
3339			return new Promise((resolve) => {
3340				this.#selectionWait = { resolve };
3341				this.#renderCheckTypeSelectionScreen();
3342			});
3343		}
3344
3345		/**
3346		 * チェックを実行して結果を表示する。
3347		 * @return null(成功 or abort)、または string(エラーメッセージ。呼び出し側で showError へ渡す)
3348		 */
3349		async #runCheckAndShowResults({ selectedTypes, customPrompt }) {
3350			this.#showLoading();
3351
3352			this.#elementMap = this.#pageElementCollector.collect('#cms_contents_area') ?? new Map();
3353
3354			const pageElements = [...this.#elementMap].map(([id, { text }]) => ({ id, text }));
3355			const result = await this.#checkApiClient.check(pageElements, selectedTypes, customPrompt);
3356
3357			if (result.aborted) return null;
3358			if (!result.ok) return result.message;
3359
3360			const results = result.issues;
3361			this.#issueListRenderer.showResults(results);
3362			if (results.length > 0) {
3363				this.#issueOverlayController.show(results, this.#elementMap);
3364			} else {
3365				this.#issueOverlayController.destroy();
3366			}
3367			return null;
3368		}
3369
3370		// ========== チェック項目選択画面 ==========
3371
3372		#renderCheckTypeSelectionScreen() {
3373			const body = this.#shadowRoot.querySelector('.aiTextCheckerBody');
3374			if (!body) {
3375				const wait = this.#selectionWait;
3376				this.#selectionWait = null;
3377				wait?.resolve(null);
3378				return;
3379			}
3380
3381			// 既にフォーム構築済みなら再構築しない(入力保持。エラー再選択時はループでここに戻ってくるため)。
3382			// 開始ボタンの click ハンドラは this.#selectionWait を都度参照するので、新しい Promise の resolve も拾える。
3383			if (body.querySelector('.aiTextCheckerSelectScreen')) {
3384				return;
3385			}
3386
3387			let listHtml = '';
3388			for (const [key, config] of Object.entries(aiTextChecker_checkTypes)) {
3389				const checked = config.default ? 'checked' : '';
3390				const infoHtml = config.description ? `
3391						<span class="aiTextCheckerCheckTypeInfo" tabindex="0" aria-label="${_h(config.label)}の説明">
3392							<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><line x1="12" y1="11" x2="12" y2="16"/><line x1="12" y1="8" x2="12" y2="8"/></svg>
3393							<span class="aiTextCheckerCheckTypeTooltip" role="tooltip">${_h(config.description)}</span>
3394						</span>
3395				` : '';
3396				listHtml += `
3397					<li class="aiTextCheckerCheckTypeItem">
3398						<label>
3399							<input type="checkbox" name="checkType" value="${key}" ${checked}>
3400							<span>${_h(config.label)}</span>
3401						</label>
3402						${infoHtml}
3403					</li>
3404				`;
3405			}
3406
3407			body.innerHTML = `
3408				<div class="aiTextCheckerSelectScreen">
3409					<div class="aiTextCheckerScopeNote">
3410						<span class="aiTextCheckerScopeNoteText">このチェックは現在画面に表示されている「ダイレクト編集」できるテキストが対象です。固定の装飾文言や画像内の文字などは含まれません。</span>
3411					</div>
3412					<p class="aiTextCheckerSelectDesc">チェックしたい項目を選択してください</p>
3413					<ul class="aiTextCheckerCheckTypeList">${listHtml}</ul>
3414					<div class="aiTextCheckerCustomSection">
3415						<label class="aiTextCheckerCustomLabel">カスタムチェックを追加(任意)</label>
3416						<textarea class="aiTextCheckerCustomInput" name="customPrompt" placeholder="ä¾
3416‹: \n・「お客様」と「お客さま」が混在していないことをチェック\n・自社名が「サンプル株式会社」ではなく「株式会社サンプル」で統一されていることをチェック\n・禁止ワード「最安値」が含まれていないことをチェック"></textarea>
3417					</div>
3418					<button class="aiTextCheckerStartBtn">チェック開始</button>
3419				</div>
3420			`;
3421
3422			const startBtn = body.querySelector('.aiTextCheckerStartBtn');
3423			const checkboxes = body.querySelectorAll('input[name="checkType"]');
3424			const customInput = body.querySelector('.aiTextCheckerCustomInput');
3425
3426			const updateBtnState = () => {
3427				const anyChecked = Array.from(checkboxes).some(cb => cb.checked);
3428				const hasCustom = customInput.value.trim() !== '';
3429				startBtn.disabled = !anyChecked && !hasCustom;
3430			};
3431			checkboxes.forEach(cb => cb.addEventListener('change', updateBtnState));
3432			customInput.addEventListener('input', updateBtnState);
3433			updateBtnState();
3434
3435			startBtn.addEventListener('click', () => {
3436				const selectedTypes = Array.from(checkboxes)
3437					.filter(cb => cb.checked)
3438					.map(cb => cb.value);
3439				const customPrompt = customInput.value.trim();
3440
3441				const wait = this.#selectionWait;
3442				this.#selectionWait = null;
3443				wait?.resolve({ selectedTypes, customPrompt });
3444			});
3445		}
3446
3447		// ========== 結果リスト表示(モーダル内) ==========
3448
3449		#enterPanelMode() {
3450			if (!this.#modal) return;
3451
3452			// 完了演出: 中央モーダルを「瞬時にタブだけの状態」へ退避し、右からゆっくりスライドインさせる。
3453			// (右に隠せる/出せるUIであることをそれとなく伝えるための見せ方)
3454
3455			// 1. transition を無効化したまま パネル+最小化 を一括適用 → 隠れる動きは見せず一瞬でタブだけに
3456			this.#modal.classList.add('aiTextCheckerNoAnim', 'aiTextCheckerModalPanel', 'aiTextCheckerModalMinimized');
3457			if (this.#overlay) this.#overlay.classList.add('aiTextCheckerOverlayTransparent');
3458			this.#setTabHandleState(true);
3459
3460			// 2. reflow を強制して最小化状態を確定させてから transition を復帰
3461			void this.#modal.offsetWidth;
3462			this.#modal.classList.remove('aiTextCheckerNoAnim');
3463
3464			// 3. 次フレームで最小化を解除 → 右からスライドイン(Entering で速度を調整)
3465			this.#modal.classList.add('aiTextCheckerModalEntering');
3466			requestAnimationFrame(() => {
3467				if (!this.#modal) return;
3468				this.#modal.classList.remove('aiTextCheckerModalMinimized');
3469				this.#setTabHandleState(false);
3470				const onEnd = (e) => {
3471					if (e.propertyName !== 'transform') return;
3472					this.#modal.classList.remove('aiTextCheckerModalEntering');
3473					this.#modal.removeEventListener('transitionend', onEnd);
3474				};
3475				this.#modal.addEventListener('transitionend', onEnd);
3476			});
3477		}
3478
3479		// ========== モーダル ==========
3480
3481		async #initModal() {
3482			const doc = window.top.document;
3483
3484			doc.getElementById('aiTextCheckerModal')?.remove();
3485			doc.getElementById('aiTextCheckerOverlay')?.remove();
3486			doc.getElementById('aiTextCheckerShadowHost')?.remove();
3487			// 前インスタンスが top document に残したチップ/スポットライトも念のため除去する(二重表示防止の保険)
3488			doc.getElementById('aiTextCheckerChipLayer')?.remove();
3489			doc.getElementById('aiTextCheckerSpotlight')?.remove();
3490
3491			this.#shadowHost = doc.createElement('div');
3492			this.#shadowHost.id = 'aiTextCheckerShadowHost';
3493			// Shadow DOM 内部の z-index は Shadow Host 外には影響しないため、
3494			// Host 自体を最前面の stacking context にする。
3495			// CMSの右クリックメニュー(.cms_context_menu)は実 computed z-index が 2147483643 (int32 max付近) のため、
3496			// それを上回るために z-index は int32 最大値の 2147483647 を使う。
3497			this.#shadowHost.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;';
3498			doc.body.appendChild(this.#shadowHost);
3499
3500			this.#shadowRoot = this.#shadowHost.attachShadow({ mode: 'open' });
3501
3502			const style = document.createElement('link');
3503			style.rel = 'stylesheet';
3504			style.href = this.#modalCssPath + '?t=1790968415';
3505
3506			await new Promise(resolve => {
3507				style.onload = resolve;
3508				style.onerror = resolve;
3509				this.#shadowRoot.appendChild(style);
3510			});
3511
3512			// チップ・チェック範囲ハイライトは Shadow 外(ページ全体スコープ)に描画されるため、
3513			// top の head にグローバルスタイルを読み込む(再オープン時の二重読み込みは防ぐ)
3514			if (!doc.getElementById('aiTextCheckerGlobalStyle')) {
3515				const globalStyle = doc.createElement('link');
3516				globalStyle.id = 'aiTextCheckerGlobalStyle';
3517				globalStyle.rel = 'stylesheet';
3518				globalStyle.href = this.#globalCssPath + '?t=1790968415';
3519				await new Promise(resolve => {
3520					globalStyle.onload = resolve;
3521					globalStyle.onerror = resolve;
3522					doc.head.appendChild(globalStyle);
3523				});
3524			}
3525
3526			this.#overlay = document.createElement('div');
3527			this.#overlay.id = 'aiTextCheckerOverlay';
3528			// overlay クリックでは閉じない(右上の×ボタンからのみ閉じる仕様)
3529			this.#shadowRoot.appendChild(this.#overlay);
3530
3531			this.#modal = document.createElement('div');
3532			this.#modal.id = 'aiTextCheckerModal';
3533			this.#modal.innerHTML = `
3534				<button class="aiTextCheckerTabHandle" type="button" title="折りたたむ" aria-label="折りたたむ"><span class="aiTextCheckerTabHandleTriangle"></span></button>
3535				<div class="aiTextCheckerHeader">
3536					<h3>
3537						<span class="aiTextCheckerHeaderTitleText">ゴーゴーAIテキストチェック</span>
3538						<span class="aiTextCheckerBetaBadge" aria-label="ベータ版">
3539							<span class="aiTextCheckerBetaBadgeIcon"><img src="/_administrator/css/default/images/ai/logo-gogoai.svg" alt="AI"></span>
3540							<span class="aiTextCheckerBetaBadgeBody"><span class="aiTextCheckerBetaBadgeText">BETA</span></span>
3541						</span>
3542					</h3>
3543					<div class="aiTextCheckerHeaderBtns">
3544						<span class="aiTextCheckerHelpBtn aiTextCheckerMovieBtn" hidden>
3545							<svg>
3546								<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-movie-manual"/>
3547							</svg>
3548						</span>
3549						<span class="aiTextCheckerHelpBtn aiTextCheckerManualBtn" hidden>
3550							<svg>
3551								<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-help"/>
3552							</svg>
3553						</span>
3554						<a class="aiTextCheckerCloseBtn">
3555							<svg>
3556								<use xlink:href="/_administrator/css/default/images/svg/symbol-defs.svg#icon-close"/>
3557							</svg>
3558						</a>
3559					</div>
3560				</div>
3561				<div class="aiTextCheckerBody"></div>
3562				<div class="aiTextCheckerFooter">本チェック結果はAIによる参考情報であり、法的助言・法律事務の提供を行うものではありません。すべてのリスクを検出することを保証するものではなく、出力結果ã
3562®æŽ¡å¦ãŠã‚ˆã³è¡¨ç¾ã®åˆ¤æ–­ãƒ»è²¬ä»»ã¯åˆ©ç”¨è€…に帰属します。<br>最終的な判断は法務担当者または弁護士にご確認ください。</div>
3563			`;
3564			this.#shadowRoot.appendChild(this.#modal);
3565
3566			this.#modal.querySelector('.aiTextCheckerCloseBtn').addEventListener('click', () => this.#requestClose());
3567			this.#modal.querySelector('.aiTextCheckerTabHandle').addEventListener('click', () => this.#toggleMinimize());
3568			this.#setupManualLinks();
3569		}
3570
3571		#setupManualLinks() {
3572			const movieBtn = this.#modal.querySelector('.aiTextCheckerMovieBtn');
3573			fetch(aiTextChecker_videoManualApiURL)
3574				.then((res) => (res.ok ? res.text() : ''))
3575				.then((movieId) => {
3576					if (!movieId) return;
3577					movieBtn.addEventListener('click', () => OpenSubWin('https://www.youtube.com/embed/' + _uc(movieId) + '/?rel=0', 920, 520, 'movie_manual'));
3578					movieBtn.hidden = false;
3579				})
3580				.catch(() => {});
3581			const manualBtn = this.#modal.querySelector('.aiTextCheckerManualBtn');
3582			fetch(aiTextChecker_helpCategoryApiURL)
3583				.then((res) => (res.ok ? res.text() : ''))
3584				.then((helpPath) => {
3585					if (!helpPath) return;
3586					manualBtn.addEventListener('click', () => OpenSubWin(aiTextChecker_helpPageBaseURL + _us(helpPath), 760, 860, 'help'));
3587					manualBtn.hidden = false;
3588				})
3589				.catch(() => {});
3590		}
3591
3592		#toggleMinimize() {
3593			if (!this.#modal) return;
3594			const minimized = this.#modal.classList.toggle('aiTextCheckerModalMinimized');
3595			this.#setTabHandleState(minimized);
3596		}
3597
3598		// タブハンドルのラベルを最小化状態に合わせて切り替える(アイコンの向きは CSS 側で反転)
3599		#setTabHandleState(minimized) {
3600			if (!this.#modal) return;
3601			const tab = this.#modal.querySelector('.aiTextCheckerTabHandle');
3602			if (!tab) return;
3603			tab.title = minimized ? '展開する' : '折りたたむ';
3604			tab.setAttribute('aria-label', minimized ? '展開する' : '折りたたむ');
3605		}
3606
3607		#showLoading() {
3608			const body = this.#shadowRoot.querySelector('.aiTextCheckerBody');
3609			if (!body) return;
3610			const selectScreen = body.querySelector('.aiTextCheckerSelectScreen');
3611			if (selectScreen) selectScreen.style.display = 'none';
3612			if (!body.querySelector('.aiTextCheckerLoading')) {
3613				const loading = document.createElement('div');
3614				loading.className = 'aiTextCheckerLoading';
3615				loading.innerHTML = `
3616					<div class="aiTextCheckerLoadingIcon"></div>
3617					<div class="aiTextCheckerLoadingText">AIがテキストをチェックしています...</div>
3618				`;
3619				body.appendChild(loading);
3620			}
3621		}
3622
3623		#hideLoading() {
3624			const body = this.#shadowRoot.querySelector('.aiTextCheckerBody');
3625			if (!body) return;
3626			const loading = body.querySelector('.aiTextCheckerLoading');
3627			if (loading) loading.remove();
3628			const selectScreen = body.querySelector('.aiTextCheckerSelectScreen');
3629			if (selectScreen) selectScreen.style.display = '';
3630		}
3631
3632		// エラーを alert_dialog で表示する。モーダルは閉じず、ローディングだけ解除してフォームを再表示する。
3633		// 表示中は shadowHost を visibility:hidden で退避させ、OK 押下時に戻す(#confirmDiscardResults と同パターン)。
3634		// OK 押下を待ってから呼び出し側(open() のループ)が再選択画面に戻る。
3635		#showError(message) {
3636			this.#hideLoading();
3637			return new Promise((resolve) => {
3638				const dialog = window.top && window.top.alert_dialog;
3639				const host = this.#shadowHost;
3640				if (typeof dialog === 'function') {
3641					if (host) host.style.visibility = 'hidden';
3642					dialog(message, undefined, () => {
3643						if (host) host.style.visibility = '';
3644						resolve();
3645						return true;
3646					});
3647					return;
3648				}
3649				alert('エラーが発生しました。');
3650				resolve();
3651			});
3652		}
3653
3654		// 結果表示中(パネルモード)かどうか。aiTextCheckerModalPanel クラスはチェック完了時に付与される。
3655		#hasResults() {
3656			return !!(this.#modal && this.#modal.classList.contains('aiTextCheckerModalPanel'));
3657		}
3658
3659		// 結果破棄確認ダイアログ。確認ダイアログが結果パネルに覆われてしまうため、
3660		// 表示中は shadowHost を visibility:hidden で退避させ、キャンセル時に戻す。
3661		// (cancel コールバックを取れるよう Dialog.confirm を直接呼ぶ)
3662		#confirmDiscardResults(message, onOk) {
3663			const topWin = window.top;
3664			const dialog = topWin && topWin.Dialog;
3665			if (!dialog || typeof dialog.confirm !== 'function') return false;
3666			const host = this.#shadowHost;
3667			if (host) host.style.visibility = 'hidden';
3668			if (topWin.jQuery && topWin.jQuery('#alert').length > 0) {
3669				topWin.jQuery('#alert').attr('id', 'alert-bf');
3670			}
3671			dialog.confirm(message, {
3672				windowParameters: { width: 480, height: 150, zIndex: 1000000, duration: 0.1 },
3673				className: 'alert',
3674				id: 'alert',
3675				okLabel: 'OK',
3676				ok: () => { onOk(); return true; },
3677				cancelLabel: 'キャンセル',
3678				buttonClass: 'cms-btn',
3679				cancel: () => { if (host) host.style.visibility = ''; }
3680			});
3681			return true;
3682		}
3683
3684		// ×ボタン経由の閉じる要求。結果表示中は確認ダイアログを挟む。
3685		#requestClose() {
3686			if (this.#hasResults()) {
3687				if (this.#confirmDiscardResults('表示中のチェック結果は失われますが、よろしいですか?', () => this.#closeModal())) {
3688					return;
3689				}
3690			}
3691			this.#closeModal();
3692		}
3693
3694		#closeModal() {
3695			if (this.#selectionWait) {
3696				this.#selectionWait.resolve(null);
3697				this.#selectionWait = null;
3698			}
3699			if (aiTextChecker.#activeInstance === this) {
3700				aiTextChecker.#activeInstance = null;
3701			}
3702			this.#checkApiClient.abort();
3703			this.#issueOverlayController.destroy();
3704			if (this.#shadowHost) {
3705				this.#shadowHost.remove();
3706				this.#shadowHost = null;
3707				this.#shadowRoot = null;
3708				this.#modal = null;
3709				this.#overlay = null;
3710			}
3711		}
3712
3713	};
3714
3715	class pageElementCollector {
3716		collect(rootId){
3717			const root = document.querySelector(rootId);
3718			if (!root) {
3719				return new Map();
3720			}
3721
3722			const consumed = new WeakSet(); // 重複回避のための処理済み要素セット
3723			const elementMap = new Map();
3724
3725			// cms-editor-* / cms-inlineedit-* : 通常のダイレクト編集要素単位
3726			// EDIT-navigation-{数値} : navigation だけは cms-editor を介さず EDIT- 自体が編集単位になる特殊モジュールなので明示的に含める
3727			// ※ EDIT-navigation-main / -footer などはコンテナなので除外し、ダイレクト編集単位の EDIT-navigation-{数値} のみ採用
3728			// 【参考】context_menuV2.class.php onmouseover_init()
3729			const EXISTING_ID_SEL = '[id^="cms-editor-"],[id^="cms-inlineedit-"],[id^="EDIT-navigation-"]';
3730			const EDIT_NAV_LEAF_RE = /^EDIT-navigation-\d+$/;
3731
3732			// テキストは #flattenWithMap で抽出する
3733			root.querySelectorAll(EXISTING_ID_SEL).forEach(el => {
3734
3735				// EDIT-navigation-{数値以外}は除外
3736				if (el.id.startsWith('EDIT-navigation-') && !EDIT_NAV_LEAF_RE.test(el.id)) {
3737					return;
3738				}
3739				// 重複回避
3740				if (consumed.has(el)) {
3741					return;
3742				}
3743				// ダイレクト編集できない要素は対象外(例: サイト設定のコピーライトなど edit_easy 権限がないモジュール)
3744				if (!this.#isDirectEditable(el)) {
3745					return;
3746				}
3747
3748				const text = this.#flattenWithMap(el).text;
3749
3750				// テキストが空なら対象外(例: 画像だけのダイレクト編集や、空のブロック要素など)
3751				if (!text) {
3752					return;
3753				}
3754
3755				// ID採番し、実DOM要素と抽出テキストをまとめて登録する
3756				const id = 'e' + elementMap.size;
3757				elementMap.set(id, { el, text });
3758
3759				// 処理済みセットに入れる。子孫要素も「処理済み」として登録し、重複処理を回避する
3760				consumed.add(el);
3761				el.querySelectorAll(EXISTING_ID_SEL).forEach(c => consumed.add(c));
3762			});
3763
3764			return elementMap;
3765		}
3766
3767		resolve(map, id){
3768			if (!id || !map) return null;
3769			const entry = map.get(id);
3770			return entry ? entry.el : null;
3771		}
3772
3773		// 指摘要素 el の中から original 文字列に対応する Range を組み立てる。
3774		// テキスト抽出は #flattenWithMap に集約(送信側 #collectPageElements と同一ロジック)。
3775		// 見つからなければ null を返す(その指摘はハイライトをスキップする)。
3776		buildIssueRange(el, original) {
3777			const want = (original || '').replace(/\s+/g, ' ').trim();
3778			if (!el || !want) return null;
3779
3780			const { text, boundaryMap } = this.#flattenWithMap(el);
3781			const startIndex = text.indexOf(want);
3782			if (startIndex < 0) return null; // 同一 original が複数ある場合は先頭一致のみ採用
3783
3784			// 例: text が "あ い" で、want が "い" なら startIndex は 2。
3785			// "い" のハイライト範囲は「2文字目の前」から「2文字目の後」までなので、
3786			// boundaryMap[2] から boundaryMap[3] までを Range にする。
3787			const start = boundaryMap[startIndex];
3788			const end = boundaryMap[startIndex + want.length];
3789			if (!start || !end) return null;
3790
3791			const range = document.createRange();
3792			range.setStart(start.node, start.offset);
3793			range.setEnd(end.node, end.offset);
3794			return range;
3795		}
3796
3797		/**
3798		 * この関数の目的:
3799		 * 「AIが読んだ文字列」と「実際のDOM上の位置」をあとで結び直せるようにする。
3800		 *
3801		 * AIにはDOMをそのまま渡せないので、まず表示テキストだけを1本の文字列 text にする。
3802		 * ただし、この時点で以下のような加工が入る。
3803		 *
3804		 *   <br> やブロック要素の区切り → 半角スペース1つにする
3805		 *   改行、タブ、連続スペース       → 半角スペース1つにする
3806		 *   非表示要素や script/style     → 読まない
3807		 *
3808		 * つまり AI が見る text は、DOM内のテキストをそのまま連結したものではない。
3809		 *
3810		 * そこで text を作りながら、boundaryMap も同時に作る。
3811		 * boundaryMap は「text の各文字の前後が、元DOMのどこに当たるか」のメモ。
3812		 *
3813		 * 例:
3814		 *   DOM : <div>あ<br>い</div>
3815		 *   text: "あ い"
3816		 *
3817		 *   text の位置:
3818		 *     0文字目 = "あ"
3819		 *     1文字目 = " "  (<br> から作ったスペース)
3820		 *     2文字目 = "い"
3821		 *
3822		 *   boundaryMap:
3823		 *     [0] "あ" の前
3824		 *     [1] "あ" の後
3825		 *     [2] "い" の前
3826		 *     [3] "い" の後
3827		 *
3828		 * たとえば AI の指摘文字が "い" なら、text.indexOf("い") は 2。
3829		 * "い" は1文字なので、ハイライト範囲は boundaryMap[2] から boundaryMap[3] まで。
3830		 *
3831		 * これで、AIが見た text 上の位置から、ブラウザが必要とする DOM Range に戻せる。
3832		 */
3833		#flattenWithMap(root) {
3834			// DOMを読みながら、この4つを更新していく。
3835			const state = {
3836				text: '',                 // AI に送る文字列。例: "あ い"
3837				boundaryMap: [],          // text の前後の位置を、DOM上の位置として覚えた配列
3838				prevIsSpace: true,        // true の間は、次のスペースを追加しない
3839				pendingSeparator: false,  // <br> などを見つけたが、まだスペースを入れていない状態
3840			};
3841
3842			// DOMノードを1つ受け取り、テキストなら文字を読み、要素なら中身を再帰的に読む。
3843			const walk = (node) => {
3844				if (node.nodeType === Node.ELEMENT_NODE) {
3845					walkElement(node);
3846					return;
3847				}
3848
3849				if (node.nodeType === Node.TEXT_NODE) {
3850					walkTextNode(node);
3851				}
3852			};
3853
3854			// 要素ノードを読む。div や p などのブロック要素は、前後にスペースが必要になる。
3855			const walkElement = (el) => {
3856				const tag = el.tagName;
3857
3858				// script/style 系は無視
3859				if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT' || tag === 'TEMPLATE') {
3860					return;
3861				}
3862
3863				const style = getComputedStyle(el);
3864
3865				// display:contents は自身のボックスを持たず getClientRects() が空・opacity も効かないが、
3866				// 子は通常どおり表示されるため不可視扱いにしない(子テキストを取りこぼさないため除外する)。
3867				const isContents = style.display === 'contents';
3868
3869				// 非表示要素は子も含めて無視
3870				if (
3871					style.display === 'none'
3872					|| style.visibility === 'hidden'
3873					|| style.contentVisibility === 'hidden'
3874					|| (!isContents && el.getClientRects().length === 0)
3875				) {
3876					return;
3877				}
3878
3879				// <br> は「ここで改行している」という印なので、次の文字の前にスペースを入れる予約をする
3880				if (tag === 'BR') {
3881					state.pendingSeparator = true;
3882					return;
3883				}
3884
3885				// span のような inline / display:contents は文の途中なので区切らない。それ以外はブロック扱い。
3886				const block = !(isContents || style.display.startsWith('inline'));
3887
3888				// 例: 「前の文字」+「<div>の中の文字」がくっつかないようにする。
3889				if (block) {
3890					state.pendingSeparator = true;
3891				}
3892
3893				// 子を順番に読む。span などのインライン要素は、普通の文の途中として読まれる。
3894				for (const child of el.childNodes) {
3895					walk(child);
3896				}
3897
3898				// 例: 「<div>の中の文字」+「後ろの文字」もくっつかないようにする。
3899				if (block) {
3900					state.pendingSeparator = true;
3901				}
3902			};
3903
3904			// テキストノードの文字を1つずつ読む。
3905			// i は「このテキストノード内の何文字目か」で、あとで Range の offset に使う。
3906			const walkTextNode = (node) => {
3907				const value = node.nodeValue ?? '';
3908
3909				for (let i = 0; i < value.length; i++) {
3910					const char = value[i];
3911
3912					// 空白文字は全部まとめて、text には半角スペース1つだけ入れる。
3913					if (/\s/.test(char)) {
3914						if (!state.prevIsSpace) {
3915							appendChar(' ', { node, offset: i }, { node, offset: i + 1 });
3916							state.prevIsSpace = true;
3917						}
3918						continue;
3919					}
3920
3921					// <br> やブロック境界で予約していたスペースを、この実文字の前に入れる。
3922					flushSeparatorBefore({
3923						node,
3924						offset: i,
3925					});
3926
3927					// 実際の文字を text に追加し、この文字の前後の DOM 位置も覚える。
3928					appendRealChar(char, {
3929						node,
3930						offset: i,
3931					}, {
3932						node,
3933						offset: i + 1,
3934					});
3935				}
3936			};
3937
3938			// 予約していた区切りを、半角スペース1つとして text に入れる。
3939			// <br> 由来のスペースはDOM上の実文字ではないので、前後の位置は同じ point にする。
3940			const flushSeparatorBefore = (point) => {
3941				if (!state.pendingSeparator) {
3942					return;
3943				}
3944
3945				if (!state.prevIsSpace) {
3946					appendChar(' ', point, point);
3947					state.prevIsSpace = true;
3948				}
3949				state.pendingSeparator = false;
3950			};
3951
3952			// 実文字を1つ追加する。追加後は「直前は空白ではない」状態になる。
3953			const appendRealChar = (char, beforePoint, afterPoint) => {
3954				appendChar(char, beforePoint, afterPoint);
3955				state.prevIsSpace = false;
3956				state.pendingSeparator = false;
3957			};
3958
3959			// text に1文字追加し、その文字の後ろの位置を boundaryMap に追加する。
3960			// 最初の文字だけは、文字の前の位置も先に入れる。
3961			//
3962			// 例: "あ" を追加する
3963			//   boundaryMap = ["あ"の前, "あ"の後]
3964			//
3965			// 次に "い" を追加する
3966			//   boundaryMap = ["あ"の前, "あ"の後, "い"の後]
3967			const appendChar = (char, beforePoint, afterPoint) => {
3968				if (state.boundaryMap.length === 0) {
3969					state.boundaryMap.push(beforePoint);
3970				}
3971
3972				state.text += char;
3973				state.boundaryMap.push(afterPoint);
3974			};
3975
3976			// 末尾スペースを削ったら、対応する最後の境界も一緒に削る。
3977			const trimTrailingSpaces = () => {
3978				while (/\s$/.test(state.text)) {
3979					state.text = state.text.slice(0, -1);
3980					state.boundaryMap.pop();
3981				}
3982			};
3983
3984			walk(root);
3985			trimTrailingSpaces();
3986
3987			return {
3988				text: state.text,
3989				boundaryMap: state.boundaryMap,
3990			};
3991		}
3992
3993
3994		// ダイレクト編集が可能な要素かを判定する。
3995		// EDIT- 祖先(自身含む)の id 第2セグメントをモジュール名とし、modules[モジュール名+'_manager'](
3995auth.php 由来)に 'edit_easy' があるか確認する。
3996		// 【参考】context_menuV2.class.php inline_edit() / onmouseover_init() / ondblclick_init() の edit_easy 判定
3997		#isDirectEditable(el) {
3998			// modules は context_menuV2.class.php 冒頭で出力されるグローバル変数。編集サイト画面以外では存在しない
3999			if (typeof modules === 'undefined') {
4000				return false;
4001			}
4002			if (typeof context_menu === 'undefined' || typeof context_menu.search_parent_node_by_id !== 'function') {
4003				return false;
4004			}
4005			const parent = context_menu.search_parent_node_by_id(el,'EDIT-');
4006			if(!parent) {
4007				return false;
4008			}
4009			const menuId = parent.id.split('-')[1] || null;
4010			if (!menuId) {
4011				return false;
4012			}
4013			const auth = modules[menuId + '_manager'];
4014			if (!Array.isArray(auth)) {
4015				return false;
4016			}
4017			return auth.indexOf('edit_easy') >= 0;
4018		}
4019	}
4020
4021	class checkApiClient {
4022		#abortController = null;
4023
4024		async check(pageElements, selectedTypes, customPrompt) {
4025			this.#abortController?.abort();
4026			const controller = new AbortController();
4027			this.#abortController = controller;
4028
4029			try {
4030				const formData = new FormData();
4031				formData.append('q', 'ai_text_checker_manager/check_text');
4032				formData.append('do', 'ai_text_checker_manager/check_text');
4033				formData.append('page_elements', JSON.stringify(pageElements));
4034				selectedTypes.forEach(type => formData.append('check_types[]', type));
4035				formData.append('custom_prompt', customPrompt || '');
4036
4037				const response = await fetch('/_administrator/connector.php', {
4038					method: 'POST',
4039					body: formData,
4040					credentials: 'same-origin',
4041					signal: controller.signal,
4042					headers: {
4043						'X-Requested-With': 'XMLHttpRequest',
4044					},
4045				});
4046
4047				if (!response.ok) {
4048					return { ok: false, message: '予期せぬエラーが発生しました。' };
4049				}
4050
4051				const data = await response.json();
4052
4053				if (!data || !data.success) {
4054					return { ok: false, message: (data && data.message) || '予期せぬエラーが発生しました。' };
4055				}
4056
4057				if (Array.isArray(data.issues)) {
4058					return { ok: true, issues: data.issues };
4059				}
4060
4061				return { ok: false, message: '予期せぬエラーが発生しました。' };
4062			} catch (e) {
4063				if (e.name === 'AbortError') {
4064					return { ok: false, aborted: true };
4065				}
4066				return { ok: false, message: '予期せぬエラーが発生しました。' };
4067			}
4068		}
4069
4070		abort() {
4071			if (this.#abortController) {
4072				this.#abortController.abort();
4073				this.#abortController = null;
4074			}
4075		}
4076	}
4077
4078	class issueListRenderer {
4079		#shadowRoot = null;
4080		#checkTypeLabels = null;
4081		#onJumpToElement = null;
4082		#onPanelModeRequired = null;
4083		#onResolvedChange = null;
4084
4085		constructor({
4086			shadowRoot,
4087			checkTypeLabels,
4088			onPanelModeRequired,
4089			onJumpToElement,
4090			onResolvedChange,
4091		}) {
4092			this.#shadowRoot = shadowRoot;
4093			this.#checkTypeLabels = checkTypeLabels;
4094			this.#onPanelModeRequired = onPanelModeRequired;
4095			this.#onJumpToElement = onJumpToElement;
4096			this.#onResolvedChange = onResolvedChange;
4097		}
4098
4099		showResults(results) {
4100			if (results.length === 0) {
4101				this.#showNoIssue();
4102				return;
4103			}
4104
4105			this.#showIssueList(results);
4106		}
4107
4108		#showNoIssue() {
4109			const body = this.#shadowRoot.querySelector('.aiTextCheckerBody');
4110			if (!body) return;
4111
4112			body.innerHTML = `
4113				<div class="aiTextCheckerNoIssue">
4114					<div class="aiTextCheckerNoIssueSparkles" aria-hidden="true">
4115						<span class="aiTextCheckerNoIssueSparkle"></span>
4116						<span class="aiTextCheckerNoIssueSparkle"></span>
4117						<span class="aiTextCheckerNoIssueSparkle"></span>
4118						<span class="aiTextCheckerNoIssueSparkle"></span>
4119						<span class="aiTextCheckerNoIssueSparkle"></span>
4120						<span class="aiTextCheckerNoIssueSparkle"></span>
4121						<span class="aiTextCheckerNoIssueSparkle"></span>
4122						<span class="aiTextCheckerNoIssueSparkle"></span>
4123					</div>
4124					<div class="aiTextCheckerNoIssueStage" aria-hidden="true">
4125						<svg viewBox="0 0 320 200" width="320" height="200" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
4126							<defs>
4127								<linearGradient id="aiTextCheckerPopperCone" x1="0%" y1="100%" x2="100%" y2="0%">
4128									<stop offset="0%" stop-color="#DA5765"/>
4129									<stop offset="55%" stop-color="#ED7C33"/>
4130									<stop offset="100%" stop-color="#FFA000"/>
4131								</linearGradient>
4132							</defs>
4133							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:1;--ai-tcp-x:130px;--ai-tcp-y:30px;--ai-tcp-dx:-40px;--ai-tcp-dy:30px;--ai-tcp-rot:14deg;--ai-tcp-fall:90px">
4134								<rect x="125" y="25" width="10" height="10" rx="2" fill="#DA5765" transform="rotate(18 130 30)"/>
4135							</g>
4136							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:2;--ai-tcp-x:158px;--ai-tcp-y:18px;--ai-tcp-dx:-30px;--ai-tcp-dy:22px;--ai-tcp-rot:-10deg;--ai-tcp-fall:105px">
4137								<circle cx="158" cy="18" r="4" fill="#3FA9F5"/>
4138							</g>
4139							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:3;--ai-tcp-x:108px;--ai-tcp-y:60px;--ai-tcp-dx:-45px;--ai-tcp-dy:28px;--ai-tcp-rot:24deg;--ai-tcp-fall:80px">
4140								<rect x="103" y="55" width="9" height="9" rx="2" fill="#FFD66E" transform="rotate(-18 108 60)"/>
4141							</g>
4142							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:4;--ai-tcp-x:142px;--ai-tcp-y:78px;--ai-tcp-dx:-32px;--ai-tcp-dy:20px;--ai-tcp-rot:-22deg;--ai-tcp-fall:95px">
4143								<circle cx="142" cy="78" r="4" fill="#7ED321"/>
4144							</g>
4145							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:5;--ai-tcp-x:172px;--ai-tcp-y:55px;--ai-tcp-dx:-25px;--ai-tcp-dy:18px;--ai-tcp-rot:32deg;--ai-tcp-fall:85px">
4146								<rect x="167" y="50" width="9" height="9" rx="2" fill="#9B59B6" transform="rotate(28 172 55)"/>
4147							</g>
4148							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:6;--ai-tcp-x:120px;--ai-tcp-y:95px;--ai-tcp-dx:-38px;--ai-tcp-dy:18px;--ai-tcp-rot:-30deg;--ai-tcp-fall:70px">
4149								<rect x="116" y="91" width="7" height="7" rx="1.5" fill="#E94B7C" transform="rotate(-25 119 94)"/>
4150							</g>
4151							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:7;--ai-tcp-x:154px;--ai-tcp-y:40px;--ai-tcp-dx:-30px;--ai-tcp-dy:22px;--ai-tcp-rot:0deg;--ai-tcp-fall:90px">
4152								<path d="M144 36 Q154 26 164 36 Q170 42 162 50" stroke="#FFD66E" stroke-width="2.5" fill="none" stroke-linecap="round"/>
4153							</g>
4154							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:8;--ai-tcp-x:188px;--ai-tcp-y:32px;--ai-tcp-dx:-22px;--ai-tcp-dy:25px;--ai-tcp-rot:-16deg;--ai-tcp-fall:100px">
4155								<circle cx="188" cy="32" r="3" fill="#1ABC9C"/>
4156							</g>
4157							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:9;--ai-tcp-x:128px;--ai-tcp-y:48px;--ai-tcp-dx:-36px;--ai-tcp-dy:26px;--ai-tcp-rot:18deg;--ai-tcp-fall:88px">
4158								<circle cx="128" cy="48" r="3" fill="#FFA000"/>
4159							</g>
4160							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:10;--ai-tcp-x:215px;--ai-tcp-y:30px;--ai-tcp-dx:40px;--ai-tcp-dy:30px;--ai-tcp-rot:-14deg;--ai-tcp-fall:90px">
4161								<rect x="210" y="25" width="10" height="10" rx="2" fill="#1ABC9C" transform="rotate(-18 215 30)"/>
4162							</g>
4163							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:11;--ai-tcp-x:190px;--ai-tcp-y:14px;--ai-tcp-dx:30px;--ai-tcp-dy:22px;--ai-tcp-rot:10deg;--ai-tcp-fall:108px">
4164								<circle cx="190" cy="14" r="4" fill="#9B59B6"/>
4165							</g>
4166							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:12;--ai-tcp-x:236px;--ai-tcp-y:60px;--ai-tcp-dx:45px;--ai-tcp-dy:28px;--ai-tcp-rot:-24deg;--ai-tcp-fall:80px">
4167								<rect x="231" y="55" width="9" height="9" rx="2" fill="#FFA000" transform="rotate(18 236 60)"/>
4168							</g>
4169							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:13;--ai-tcp-x:202px;--ai-tcp-y:78px;--ai-tcp-dx:32px;--ai-tcp-dy:20px;--ai-tcp-rot:22deg;--ai-tcp-fall:95px">
4170								<circle cx="202" cy="78" r="4" fill="#DA5765"/>
4171							</g>
4172							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:14;--ai-tcp-x:172px;--ai-tcp-y:55px;--ai-tcp-dx:25px;--ai-tcp-dy:18px;--ai-tcp-rot:-32deg;--ai-tcp-fall:85px">
4173								<rect x="167" y="50" width="9" height="9" rx="2" fill="#3FA9F5" transform="rotate(-28 172 55)"/>
4174							</g>
4175							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:15;--ai-tcp-x:224px;--ai-tcp-y:95px;--ai-tcp-dx:38px;--ai-tcp-dy:18px;--ai-tcp-rot:30deg;--ai-tcp-fall:70px">
4176								<rect x="220" y="91" width="7" height="7" rx="1.5" fill="#7ED321" transform="rotate(25 223 94)"/>
4177							</g>
4178							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:16;--ai-tcp-x:186px;--ai-tcp-y:40px;--ai-tcp-dx:30px;--ai-tcp-dy:22px;--ai-tcp-rot:0deg;--ai-tcp-fall:90px">
4179								<path d="M180 36 Q190 26 200 36 Q204 42 198 52" stroke="#DA5765" stroke-width="2.5" fill="none" stroke-linecap="round"/>
4180							</g>
4181							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:17;--ai-tcp-x:152px;--ai-tcp-y:32px;--ai-tcp-dx:22px;--ai-tcp-dy:25px;--ai-tcp-rot:16deg;--ai-tcp-fall:100px">
4182								<circle cx="152" cy="32" r="3" fill="#FFD66E"/>
4183							</g>
4184							<g class="aiTextCheckerNoIssuePopperPiece" style="--ai-tcp-i:18;--ai-tcp-x:212px;--ai-tcp-y:48px;--ai-tcp-dx:36px;--ai-tcp-dy:26px;--ai-tcp-rot:-18deg;--ai-tcp-fall:88px">
4185								<circle cx="212" cy="48" r="3" fill="#E94B7C"/>
4186							</g>
4187							<g class="aiTextCheckerNoIssuePopperCone aiTextCheckerNoIssuePopperConeLeft">
4188								<g transform="translate(18 188) rotate(-46)">
4189									<path d="M0 -2.5 L0 2.5 L78 25 L78 -25 Z" fill="url(#aiTextCheckerPopperCone)"/>
4190									<path d="M20 -8 L20 8" stroke="rgba(255,255,255,0.45)" stroke-width="2.2" stroke-linecap="round"/>
4191									<path d="M40 -14 L40 14" stroke="rgba(255,255,255,0.45)" stroke-width="2.2" stroke-linecap="round"/>
4192									<path d="M58 -20 L58 20" stroke="rgba(255,255,255,0.45)" stroke-width="2.2" stroke-linecap="round"/>
4193									<ellipse cx="78" cy="0" rx="3" ry="25" fill="rgba(0,0,0,0.22)"/>
4194									<circle cx="-1" cy="0" r="3.5" fill="#DA5765"/>
4195								</g>
4196							</g>
4197							<g class="aiTextCheckerNoIssuePopperCone aiTextCheckerNoIssuePopperConeRight">
4198								<g transform="translate(302 188) rotate(226)">
4199									<path d="M0 -2.5 L0 2.5 L78 25 L78 -25 Z" fill="url(#aiTextCheckerPopperCone)"/>
4200									<path d="M20 -8 L20 8" stroke="rgba(255,255,255,0.45)" stroke-width="2.2" stroke-linecap="round"/>
4201									<path d="M40 -14 L40 14" stroke="rgba(255,255,255,0.45)" stroke-width="2.2" stroke-linecap="round"/>
4202									<path d="M58 -20 L58 20" stroke="rgba(255,255,255,0.45)" stroke-width="2.2" stroke-linecap="round"/>
4203									<ellipse cx="78" cy="0" rx="3" ry="25" fill="rgba(0,0,0,0.22)"/>
4204									<circle cx="-1" cy="0" r="3.5" fill="#DA5765"/>
4205								</g>
4206							</g>
4207						</svg>
4208						<div class="aiTextCheckerNoIssueTexts">
4209							<div class="aiTextCheckerNoIssueTitle">
4210								<span class="aiTextCheckerNoIssueTitleMain">お見事!</span>
4211								<span class="aiTextCheckerNoIssueTitleSub">目立った指摘は見つかりませんでした</span>
4212							</div>
4213						</div>
4214					</div>
4215				</div>
4216			`;
4217			return;
4218		}
4219
4220		#showIssueList(results) {
4221			// パネルモードへ切替(ページが見える右サイドパネル)
4222			if (this.#onPanelModeRequired) this.#onPanelModeRequired();
4223
4224			const body = this.#shadowRoot.querySelector('.aiTextCheckerBody');
4225			if (!body) return;
4226
4227			body.innerHTML = `
4228				<div class="aiTextCheckerIssueListHeader">
4229					<div class="aiTextCheckerIssueListCount">
4230						<span class="aiTextCheckerIssueListCountNum">${results.length}</span>
4231						<span class="aiTextCheckerIssueListCountLabel">件の指摘</span>
4232						<span class="aiTextCheckerIssueListRemaining" data-remaining="${results.length}">残り ${results.length} 件</span>
4233					</div>
4234				</div>
4235				<ul class="aiTextCheckerIssueList"></ul>
4236			`;
4237
4238			const listEl = body.querySelector('.aiTextCheckerIssueList');
4239			const remainingEl = body.querySelector('.aiTextCheckerIssueListRemaining');
4240			const state = { remaining: results.length };
4241
4242			results.forEach((item, idx) => {
4243				listEl.appendChild(this.#buildIssueCard(item, idx, state, remainingEl));
4244			});
4245		}
4246
4247		#buildIssueCard(item, idx, state, remainingEl) {
4248			const card = document.createElement('li');
4249			card.className = 'aiTextCheckerIssueCard';
4250			card.setAttribute('data-target-id', item.element_id || '');
4251
4252			const checkTypeLabel = this.#checkTypeLabels[item.check_type] || '指摘';
4253
4254			const head = document.createElement('div');
4255			head.className = 'aiTextCheckerIssueCardHead';
4256			head.innerHTML = `
4257				<span class="aiTextCheckerIssueCardBadge">${idx + 1}</span>
4258				<span class="aiTextCheckerIssueCardCheckType">${_h(checkTypeLabel)}</span>
4259			`;
4260			card.appendChild(head);
4261
4262			const hasSuggested = !!(typeof item.suggested === 'string' && item.suggested.length);
4263
4264			const diff = document.createElement('div');
4265			diff.className = 'aiTextCheckerIssueCardDiff';
4266			const origSpan = document.createElement('span');
4267			origSpan.textContent = item.original || '';
4268			if (hasSuggested) {
4269				origSpan.className = 'aiTextCheckerIssueCardOriginal';
4270				const arrow = document.createElement('span');
4271				arrow.className = 'aiTextCheckerIssueCardArrow';
4272				arrow.textContent = '↓';
4273				const suggestedRow = document.createElement('div');
4274				suggestedRow.className = 'aiTextCheckerIssueCardSuggestedRow';
4275				const sugSpan = document.createElement('span');
4276				sugSpan.className = 'aiTextCheckerIssueCardSuggested';
4277				sugSpan.textContent = item.suggested;
4278				const copyBtn = document.createElement('span');
4279				copyBtn.className = 'aiTextCheckerIssueCardCopy';
4280				copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 32 32" aria-hidden="true"><path d="M24.889 10.667c0.981 0 1.778 0.796 1.778 1.778v14.222c0 0.981-0.796 1.778-1.778 1.778h-10.667c-0.981 0-1.778-0.796-1.778-1.778v-14.222c0-0.981 0.796-1.778 1.778-1.778h10.667zM24.889 7.111h-10.667c-2.94 0-5.333 2.393-5.333 5.333v14.222c0 2.94 2.393 5.333 5.333 5.333h10.667c2.94 0 5.333-2.393 5.333-5.333v-14.222c0-2.94-2.393-5.333-5.333-5.333v0z"></path><path d="M17.778 0h-10.667c-2.94 0-5.333 2.393-5.333 5.333v10.667c0 0.981 0.796 1.778 1.778 1.778s1.778-0.796 1.778-1.778v-10.667c0-0.981 0.796-1.778 1.778-1.778h10.667c0.981 0 1.778-0.796 1.778-1.778s-0.796-1.778-1.778-1.778z"></path></svg>';
4281				copyBtn.setAttribute('aria-label', '修正後のテキストをコピー');
4282				copyBtn.setAttribute('title', '修正後のテキストをコピー');
4283				const copyFeedback = document.createElement('span');
4284				copyFeedback.className = 'aiTextCheckerIssueCardCopyFeedback';
4285				copyFeedback.setAttribute('aria-live', 'polite');
4286				copyBtn.addEventListener('click', (e) => {
4287					e.stopPropagation();
4288					this.#copyToClipboard(item.suggested, copyFeedback);
4289				});
4290				suggestedRow.appendChild(sugSpan);
4291				suggestedRow.appendChild(copyBtn);
4292				suggestedRow.appendChild(copyFeedback);
4293				diff.appendChild(origSpan);
4294				diff.appendChild(arrow);
4295				diff.appendChild(suggestedRow);
4296			} else {
4297				origSpan.className = 'aiTextCheckerIssueCardTargetText';
4298				diff.appendChild(origSpan);
4299			}
4300			card.appendChild(diff);
4301
4302			if (item.description) {
4303				const description = document.createElement('div');
4304				description.className = 'aiTextCheckerIssueCardDescription';
4305				description.textContent = item.description;
4306				card.appendChild(description);
4307			}
4308
4309			const actions = document.createElement('div');
4310			actions.className = 'aiTextCheckerIssueCardActions';
4311
4312			const jumpBtn = document.createElement('button');
4313			jumpBtn.type = 'button';
4314			jumpBtn.className = 'aiTextCheckerIssueCardJumpBtn';
4315			jumpBtn.innerHTML = `
4316				<span class="aiTextCheckerIssueCardJumpBtnIcon" aria-hidden="true">
4317					<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
4318						<path d="M15 10l5 5-5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
4319						<path d="M4 4v7a4 4 0 0 0 4 4h12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
4320					</svg>
4321				</span>
4322				<span class="aiTextCheckerIssueCardJumpBtnText">該当箇所へ</span>
4323			`;
4324			jumpBtn.addEventListener('click', (e) => {
4325				e.stopPropagation();
4326				if (this.#onJumpToElement) this.#onJumpToElement(item.element_id, idx);
4327			});
4328
4329			const cardState = { resolved: false };
4330			const checkLabel = document.createElement('label');
4331			checkLabel.className = 'aiTextCheckerIssueCardCheck';
4332			const checkInput = document.createElement('input');
4333			checkInput.type = 'checkbox';
4334			checkInput.className = 'aiTextCheckerIssueCardCheckInput';
4335			const checkText = document.createElement('span');
4336			checkText.className = 'aiTextCheckerIssueCardCheckText';
4337			checkText.textContent = '確認済み';
4338			checkLabel.appendChild(checkInput);
4339			checkLabel.appendChild(checkText);
4340
4341			const updateChipReviewed = (reviewed) => {
4342				if (this.#onResolvedChange) this.#onResolvedChange(idx, reviewed);
4343			};
4344
4345			const markResolved = () => {
4346				if (cardState.resolved) return;
4347				cardState.resolved = true;
4348				if (!checkInput.checked) checkInput.checked = true;
4349				card.classList.add('aiTextCheckerIssueCardResolved', 'aiTextCheckerIssueCardCollapsed', 'aiTextCheckerIssueCardReviewed');
4350				state.remaining--;
4351				remainingEl.setAttribute('data-remaining', state.remaining);
4352				remainingEl.textContent = '残り ' + state.remaining + ' 件';
4353				updateChipReviewed(true);
4354			};
4355			const markPending = () => {
4356				if (!cardState.resolved) return;
4357				cardState.resolved = false;
4358				if (checkInput.checked) checkInput.checked = false;
4359				card.classList.remove('aiTextCheckerIssueCardResolved', 'aiTextCheckerIssueCardCollapsed', 'aiTextCheckerIssueCardReviewed');
4360				state.remaining++;
4361				remainingEl.setAttribute('data-remaining', state.remaining);
4362				remainingEl.textContent = '残り ' + state.remaining + ' 件';
4363				updateChipReviewed(false);
4364			};
4365
4366			checkInput.addEventListener('change', () => {
4367				if (checkInput.checked) markResolved();
4368				else markPending();
4369			});
4370			// コンパクト時にカード本体クリックでチェックが切り替わると操作しづらいので
4371			// label 起点のクリックがカード外に bubble しないようにする
4372			checkLabel.addEventListener('click', (e) => e.stopPropagation());
4373
4374			actions.appendChild(jumpBtn);
4375			actions.appendChild(checkLabel);
4376			card.appendChild(actions);
4377
4378			const summary = document.createElement('div');
4379			summary.className = 'aiTextCheckerIssueCardSummary';
4380			const summaryText = document.createElement('span');
4381			summaryText.className = 'aiTextCheckerIssueCardSummaryText';
4382			if (hasSuggested) {
4383				const summaryBefore = document.createElement('span');
4384				summaryBefore.className = 'aiTextCheckerIssueCardSummaryBefore';
4385				summaryBefore.textContent = item.original || '';
4386				const summaryArrow = document.createElement('span');
4387				summaryArrow.className = 'aiTextCheckerIssueCardSummaryArrow';
4388				summaryArrow.textContent = ' → ';
4389				const summaryAfter = document.createElement('span');
4390				summaryAfter.className = 'aiTextCheckerIssueCardSummaryAfter';
4391				summaryAfter.textContent = item.suggested;
4392				summaryText.appendChild(summaryBefore);
4393				summaryText.appendChild(summaryArrow);
4394				summaryText.appendChild(summaryAfter);
4395			} else {
4396				const summaryTargetLabel = document.createElement('span');
4397				summaryTargetLabel.className = 'aiTextCheckerIssueCardSummaryTargetLabel';
4398				summaryTargetLabel.textContent = '該当箇所: ';
4399				const summaryTarget = document.createElement('span');
4400				summaryTarget.className = 'aiTextCheckerIssueCardSummaryTarget';
4401				summaryTarget.textContent = item.original || '';
4402				summaryText.appendChild(summaryTargetLabel);
4403				summaryText.appendChild(summaryTarget);
4404			}
4405			summary.appendChild(summaryText);
4406			card.appendChild(summary);
4407
4408			return card;
4409		}
4410
4411		// ========== クリップボード ==========
4412
4413		async #copyToClipboard(text, feedbackEl) {
4414			let ok = false;
4415			try {
4416				if (navigator.clipboard && window.isSecureContext) {
4417					await navigator.clipboard.writeText(text);
4418					ok = true;
4419				}
4420			} catch (_) {
4421				ok = false;
4422			}
4423			if (!ok) {
4424				ok = this.#fallbackCopy(text);
4425			}
4426			this.#showCopyFeedback(feedbackEl, ok ? 'コピーしました' : 'コピーに失敗しました');
4427		}
4428
4429		#fallbackCopy(text){
4430			const doc = window.top.document;
4431			const textarea = doc.createElement('textarea');
4432			textarea.value = text;
4433			textarea.setAttribute('readonly', '');
4434			textarea.style.cssText = 'position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;';
4435			doc.body.appendChild(textarea);
4436			const selection = doc.getSelection();
4437			const prevRange = (selection && selection.rangeCount > 0) ? selection.getRangeAt(0) : null;
4438			let ok = false;
4439			try {
4440				textarea.select();
4441				textarea.setSelectionRange(0, textarea.value.length);
4442				ok = doc.execCommand('copy');
4443			} catch (_) {
4444				ok = false;
4445			} finally {
4446				doc.body.removeChild(textarea);
4447				if (prevRange && selection) {
4448					selection.removeAllRanges();
4449					selection.addRange(prevRange);
4450				}
4451			}
4452			return ok;
4453		}
4454
4455		#showCopyFeedback(feedbackEl, message){
4456			if (!feedbackEl) return;
4457			if (feedbackEl._fadeTimer) clearTimeout(feedbackEl._fadeTimer);
4458			if (feedbackEl._removeTimer) clearTimeout(feedbackEl._removeTimer);
4459			feedbackEl.textContent = message;
4460			feedbackEl.classList.remove('aiTextCheckerIssueCardCopyFeedbackFade');
4461			feedbackEl.classList.add('aiTextCheckerIssueCardCopyFeedbackVisible');
4462			feedbackEl._fadeTimer = setTimeout(() => {
4463				feedbackEl.classList.add('aiTextCheckerIssueCardCopyFeedbackFade');
4464			}, 4000);
4465			feedbackEl._removeTimer = setTimeout(() => {
4466				feedbackEl.classList.remove('aiTextCheckerIssueCardCopyFeedbackVisible', 'aiTextCheckerIssueCardCopyFeedbackFade');
4467				feedbackEl.textContent = '';
4468			}, 5000);
4469		}
4470	}
4471
4472	class issueOverlayController {
4473		#doc = null;
4474		#elementMap = null;
4475		#pageElementCollector = null;
4476		#modalAccessor = null;
4477		#chipNodes = [];
4478		#chipLayer = null;
4479		#chipReposListener = null;
4480		#chipReposRafId = 0;
4481		#chipScrollRoots = [];
4482		#spotlightSvg = null;
4483		#spotlightHoles = [];
4484		#spotlightBaseRect = null;
4485		#spotlightRoot = null;
4486		#jumpScrollCleanup = null;
4487
4488		constructor({
4489			doc,
4490			pageElementCollector,
4491			modalAccessor,
4492		}) {
4493			this.#doc = doc;
4494			this.#pageElementCollector = pageElementCollector;
4495			this.#modalAccessor = modalAccessor;
4496		}
4497
4498		show(results, elementMap) {
4499			this.#elementMap = elementMap;
4500			this.#renderElementChips(results);
4501			this.#showSpotlight();
4502			this.#markIssueHighlights(results);
4503		}
4504
4505		destroy() {
4506			this.#abortJumpScroll();
4507			this.#removeElementChips();
4508			this.#clearSpotlight();
4509			this.#clearIssueHighlights();
4510		}
4511
4512		setChipReviewed(idx, reviewed) {
4513			if (!this.#chipLayer) return;
4514			const chip = this.#chipLayer.querySelector('.aiTextCheckerIssueChip[data-idx="' + idx + '"]');
4515			if (!chip) return;
4516			if (reviewed) chip.classList.add('aiTextCheckerIssueChipReviewed');
4517			else chip.classList.remove('aiTextCheckerIssueChipReviewed');
4518		}
4519
4520		jumpToElement(el, idx) {
4521			this.#abortJumpScroll();
4522			const chip = this.#chipLayer
4523				? this.#chipLayer.querySelector('.aiTextCheckerIssueChip[data-idx="' + idx + '"]')
4524				: null;
4525
4526			const startBounce = () => {
4527				if (!chip) return;
4528				// 連続ジャンプで再生するためにクラスを一度外して reflow 後に付け直す
4529				chip.classList.remove('aiTextCheckerIssueChipBouncing');
4530				void chip.offsetWidth;
4531				chip.classList.add('aiTextCheckerIssueChipBouncing');
4532				const onEnd = () => {
4533					chip.classList.remove('aiTextCheckerIssueChipBouncing');
4534					chip.removeEventListener('animationend', onEnd);
4535				};
4536				chip.addEventListener('animationend', onEnd);
4537			};
4538
4539			// 既に画面内に完全に収まっているならスクロール不要 → 即時 bounce
4540			const rect = el.getBoundingClientRect();
4541			const viewportH = window.innerHeight || document.documentElement.clientHeight;
4542			const fullyVisible = rect.top >= 0 && rect.bottom <= viewportH;
4543			if (fullyVisible) {
4544				startBounce();
4545				return;
4546			}
4547
4548			// スクロール実行
4549			try {
4550				el.scrollIntoView({ behavior: 'smooth', block: 'center' });
4551			} catch (_) {
4552				el.scrollIntoView();
4553			}
4554
4555			// スクロール停止検知(最後の scroll イベントから 80ms 経過で完了とみなã
4555™ï¼‰
4556			let stopTimer;
4557			let fallbackTimer;
4558			const cleanup = () => {
4559				window.removeEventListener('scroll', onScroll, true);
4560				clearTimeout(stopTimer);
4561				clearTimeout(fallbackTimer);
4562				this.#jumpScrollCleanup = null;
4563			};
4564			this.#jumpScrollCleanup = cleanup;
4565			const onScroll = () => {
4566				clearTimeout(stopTimer);
4567				stopTimer = setTimeout(() => {
4568					cleanup();
4569					startBounce();
4570				}, 80);
4571			};
4572			window.addEventListener('scroll', onScroll, true);
4573			// 念のため最大 1.5 秒で強制スタート(scrollイベントが発火しない環境への保険)
4574			fallbackTimer = setTimeout(() => {
4575				cleanup();
4576				startBounce();
4577			}, 1500);
4578		}
4579
4580		#abortJumpScroll() {
4581			if (this.#jumpScrollCleanup) {
4582				this.#jumpScrollCleanup();
4583			}
4584		}
4585
4586		#renderElementChips(results) {
4587			this.#removeElementChips();
4588
4589			const doc = this.#doc;
4590			const layer = doc.createElement('div');
4591			layer.id = 'aiTextCheckerChipLayer';
4592			doc.body.appendChild(layer);
4593			this.#chipLayer = layer;
4594
4595			const idCount = {};
4596			results.forEach((item, idx) => {
4597				if (!item.element_id) return;
4598				const target = this.#pageElementCollector.resolve(this.#elementMap, item.element_id);
4599				if (!target) return;
4600
4601				const stack = idCount[item.element_id] || 0;
4602				idCount[item.element_id] = stack + 1;
4603
4604				const chip = doc.createElement('div');
4605				chip.className = 'aiTextCheckerIssueChip';
4606				chip.dataset.idx = String(idx);
4607				chip.textContent = String(idx + 1);
4608				chip.title = String(idx + 1) + ' ' + (item.original || '');
4609				chip.addEventListener('click', (e) => {
4610					e.stopPropagation();
4611					if (this.#modalAccessor.isMinimized()) {
4612						this.#modalAccessor.expand();
4613					}
4614					this.#modalAccessor.focusCard(idx);
4615					this.jumpToElement(target, idx);
4616				});
4617				layer.appendChild(chip);
4618				this.#chipNodes.push({ chip, target, stack });
4619			});
4620
4621			this.#updateChipPositions();
4622			this.#bindChipScrollListeners();
4623		}
4624
4625		#collectChipScrollRoots() {
4626			const roots = new Set();
4627			const win = this.#doc.defaultView || window;
4628			for (const { target } of this.#chipNodes) {
4629				let el = target.parentElement;
4630				while (el) {
4631					const style = win.getComputedStyle(el);
4632					if (/(auto|scroll|overlay)/.test(style.overflow + style.overflowX + style.overflowY)) {
4633						roots.add(el);
4634					}
4635					el = el.parentElement;
4636				}
4637			}
4638			return roots;
4639		}
4640
4641		#bindChipScrollListeners() {
4642			if (this.#chipReposListener) return;
4643
4644			// チップとスポットライトの穴は同じ getBoundingClientRect 追従なので 1 本のリスナーで両方更新する。
4645			// scroll は高頻度で発火するため rAF でスロットルする(多重予約を防ぐ)。
4646			this.#chipReposListener = () => {
4647				if (this.#chipReposRafId) return;
4648				this.#chipReposRafId = requestAnimationFrame(() => {
4649					this.#chipReposRafId = 0;
4650					this.#updateChipPositions();
4651					this.#updateSpotlight();
4652				});
4653			};
4654
4655			const handler = this.#chipReposListener;
4656			const win = this.#doc.defaultView || window;
4657			win.addEventListener('scroll', handler, true);
4658			win.addEventListener('resize', handler);
4659			this.#chipScrollRoots.push(win);
4660			for (const root of this.#collectChipScrollRoots()) {
4661				if (root === win) continue;
4662				root.addEventListener('scroll', handler, { passive: true });
4663				this.#chipScrollRoots.push(root);
4664			}
4665		}
4666
4667		#unbindChipScrollListeners() {
4668			const handler = this.#chipReposListener;
4669			if (!handler) return;
4670			const win = this.#doc.defaultView || window;
4671			win.removeEventListener('scroll', handler, true);
4672			win.removeEventListener('resize', handler);
4673			for (const root of this.#chipScrollRoots) {
4674				root.removeEventListener('scroll', handler);
4675			}
4676			this.#chipScrollRoots = [];
4677			this.#chipReposListener = null;
4678			if (this.#chipReposRafId) {
4679				cancelAnimationFrame(this.#chipReposRafId);
4680				this.#chipReposRafId = 0;
4681			}
4682		}
4683
4684		#updateChipPositions() {
4685			const viewportH = window.innerHeight || document.documentElement.clientHeight;
4686			const viewportW = window.innerWidth || document.documentElement.clientWidth;
4687			const chipGap = 6;
4688			// 画面端では数字が見える範囲でチップ端だけ切る。これ以上内側へずらすと指摘対象の文字に被る
4689			const clipAllowance = 3;
4690
4691			/** 同一要素への複数チップは stack 順を保ったまままとめてずらす */
4692			const groups = new Map();
4693			for (const node of this.#chipNodes) {
4694				if (!groups.has(node.target)) groups.set(node.target, []);
4695				groups.get(node.target).push(node);
4696			}
4697
4698			for (const nodes of groups.values()) {
4699				nodes.sort((a, b) => a.stack - b.stack);
4700				const rect = nodes[0].target.getBoundingClientRect();
4701				const maxChipW = Math.max(...nodes.map(({ chip }) => chip.offsetWidth || 22));
4702				const inViewport = rect.bottom > 0 && rect.top < viewportH && rect.right > 0 && rect.left < viewportW;
4703
4704				let left = rect.left - maxChipW - chipGap;
4705				if (inViewport) {
4706					if (left < -clipAllowance) {
4707						left = -clipAllowance;
4708					} else if (left + maxChipW > viewportW) {
4709						left = viewportW - maxChipW;
4710					}
4711				}
4712
4713				const tops = nodes.map(({ stack }) => rect.top - 8 + stack * 26);
4714
4715				nodes.forEach(({ chip }, i) => {
4716					chip.style.top = tops[i] + 'px';
4717					chip.style.left = left + 'px';
4718				});
4719			}
4720		}
4721
4722		#removeElementChips() {
4723			this.#unbindChipScrollListeners();
4724			if (this.#chipLayer) {
4725				this.#chipLayer.remove();
4726				this.#chipLayer = null;
4727			}
4728			this.#chipNodes = [];
4729		}
4730
4731		// ========== スポットライト(範囲外ディミング / 暗幕に穴) ==========
4732
4733		// チェック範囲(#cms_contents_area)を暗い幕で覆い、チェック対象要素の位置だけを SVG mask でくり抜く。
4734		// 「周囲を暗くして対象を浮かせる(引き算)」見せ方(以前は対象を outline で縁取る足し算方式だったが撤去)。
4735		// 暗幕は #cms_contents_area の領域だけにかけ、その外側(管理ツールUI)は暗くしない。
4736		// filter は祖先に当てると配下ごとラスタライズされ子で打ち消せないため使えないが、
4737		// SVG mask の穴方式はその制約と無関係に成立する。
4738		// 穴座標は getBoundingClientRect(viewport 基準)をそのまま使う。SVG を position:fixed にするため
4739		// スクロールオフセットの加算は不要。追従は #renderElementChips のリスナーに相乗りする。
4740		#showSpotlight() {
4741			this.#clearSpotlight();
4742			if (!this.#elementMap || this.#elementMap.size === 0) return;
4743			const root = document.querySelector('#cms_contents_area');
4744			if (!root) return;
4745
4746			const doc = this.#doc;
4747			const SVG_NS = 'http://www.w3.org/2000/svg';
4748			const maskId = 'aiTextCheckerSpotlightMask';
4749
4750			const svg = doc.createElementNS(SVG_NS, 'svg');
4751			svg.id = 'aiTextCheckerSpotlight';
4752			svg.setAttribute('preserveAspectRatio', 'none');
4753
4754			const defs = doc.createElementNS(SVG_NS, 'defs');
4755			const mask = doc.createElementNS(SVG_NS, 'mask');
4756			mask.id = maskId;
4757
4758			// white = 暗幕を表示する領域。#cms_contents_area の矩形に限定する(座標は #updateSpotlight で追従)。穴は black で抜く。
4759			const base = doc.createElementNS(SVG_NS, 'rect');
4760			base.setAttribute('fill', '#fff');
4761			mask.appendChild(base);
4762			this.#spotlightBaseRect = base;
4763			this.#spotlightRoot = root;
4764
4765			this.#spotlightHoles = [];
4766			for (const { el } of this.#elementMap.values()) {
4767				const hole = doc.createElementNS(SVG_NS, 'rect');
4768				hole.setAttribute('rx', '4');
4769				hole.setAttribute('fill', '#000');
4770				mask.appendChild(hole);
4771				this.#spotlightHoles.push({ target: el, rect: hole });
4772			}
4773
4774			// 暗幕の塗り。既存のモーダル背景オーバーレイ(#aiTextCheckerOverlay)と同じ暖色グラデを SVG で再現する。
4775			const gradId = 'aiTextCheckerSpotlightGrad';
4776			const grad = doc.createElementNS(SVG_NS, 'linearGradient');
4777			grad.id = gradId;
4778			grad.setAttribute('x1', '0');
4779			grad.setAttribute('y1', '0');
4780			grad.setAttribute('x2', '0');
4781			grad.setAttribute('y2', '1');
4782			[['0%', '#DA5765'], ['25%', '#D4584D'], ['50%', '#ED7C33'], ['75%', '#FFA000'], ['100%', '#FFD900']].forEach(([offset, color]) => {
4783				const stop = doc.createElementNS(SVG_NS, 'stop');
4784				stop.setAttribute('offset', offset);
4785				stop.setAttribute('stop-color', color);
4786				stop.setAttribute('stop-opacity', '0.8');
4787				grad.appendChild(stop);
4788			});
4789
4790			defs.appendChild(mask);
4791			defs.appendChild(grad);
4792			svg.appendChild(defs);
4793
4794			const dim = doc.createElementNS(SVG_NS, 'rect');
4795			dim.setAttribute('x', '0');
4796			dim.setAttribute('y', '0');
4797			dim.setAttribute('width', '100%');
4798			dim.setAttribute('height', '100%');
4799			dim.setAttribute('fill', 'url(#' + gradId + ')');
4800			dim.setAttribute('fill-opacity', '0.5');
4801			dim.setAttribute('mask', 'url(#' + maskId + ')');
4802			svg.appendChild(dim);
4803
4804			// グラデに加えて色味を調整するためのレイヤー
4805			// 暗さはこの fill-opacity で調整する。
4806			const colorLayer = doc.createElementNS(SVG_NS, 'rect');
4807			colorLayer.setAttribute('x', '0');
4808			colorLayer.setAttribute('y', '0');
4809			colorLayer.setAttribute('width', '100%');
4810			colorLayer.setAttribute('height', '100%');
4811			colorLayer.setAttribute('fill', '#fff');
4812			colorLayer.setAttribute('fill-opacity', '0.15');
4813			colorLayer.setAttribute('mask', 'url(#' + maskId + ')');
4814			svg.appendChild(colorLayer);
4815
4816			doc.body.appendChild(svg);
4817			this.#spotlightSvg = svg;
4818
4819			this.#updateSpotlight();
4820
4821			// フェードイン(CSS 側で transition 済み。次フレームで可視クラスを付与)
4822			requestAnimationFrame(() => {
4823				if (this.#spotlightSvg) this.#spotlightSvg.classList.add('aiTextCheckerSpotlightVisible');
4824			});
4825		}
4826
4827		#updateSpotlight() {
4828			if (!this.#spotlightSvg) return;
4829			// 暗幕領域を #cms_contents_area に追従させる(viewport 基準の矩形)
4830			let br = null;
4831			if (this.#spotlightBaseRect && this.#spotlightRoot) {
4832				br = this.#spotlightRoot.getBoundingClientRect();
4833				this.#spotlightBaseRect.setAttribute('x', br.left + '');
4834				this.#spotlightBaseRect.setAttribute('y', br.top + '');
4835				this.#spotlightBaseRect.setAttribute('width', Math.max(0, br.width) + '');
4836				this.#spotlightBaseRect.setAttribute('height', Math.max(0, br.height) + '');
4837			}
4838			const PAD = 4; // 対象の縁ギリギリだと窮屈なので少し広げる
4839			for (const { target, rect } of this.#spotlightHoles) {
4840				const r = target.getBoundingClientRect();
4841				rect.setAttribute('x', (r.left - PAD) + '');
4842				rect.setAttribute('y', (r.top - PAD) + '');
4843				rect.setAttribute('width', Math.max(0, r.width + PAD * 2) + '');
4844				rect.setAttribute('height', Math.max(0, r.height + PAD * 2) + '');
4845			}
4846		}
4847
4848		#clearSpotlight() {
4849			if (this.#spotlightSvg) {
4850				this.#spotlightSvg.remove();
4851				this.#spotlightSvg = null;
4852			}
4853			this.#spotlightHoles = [];
4854			this.#spotlightBaseRect = null;
4855			this.#spotlightRoot = null;
4856		}
4857
4858		// 指摘のある文字だけを背景ハイライトする(CSS Custom Highlight API / 非破壊)。
4859		// DOM を書き換えないのでダイレクト編集要素の中身を汚さず、リフローにも自動追従する。
4860		#markIssueHighlights(results) {
4861			this.#clearIssueHighlights();
4862			if (!(window.CSS && CSS.highlights)) {
4863				return;
4864			}
4865
4866			const ranges = [];
4867			results.forEach(item => {
4868				const el = this.#pageElementCollector.resolve(this.#elementMap, item.element_id);
4869				if (!el) {
4870					return;
4871				}
4872				const range = this.#pageElementCollector.buildIssueRange(el, item.original);
4873				if (range) {
4874					ranges.push(range);
4875				}
4876			});
4877
4878			if (ranges.length === 0) return;
4879			CSS.highlights.set('aiTextCheckerIssue', new Highlight(...ranges));
4880		}
4881
4882		#clearIssueHighlights() {
4883			if (window.CSS && CSS.highlights) {
4884				CSS.highlights.delete('aiTextCheckerIssue');
4885			}
4886		}
4887	}
4888
4889}
4890
4891// ゴーゴーAIテキストチェックを起動するフローティングボタン。
4892// 当面 AI 機能はテキストチェック1つのみのため、起動先(aiTextChecker)を直接呼ぶ。
4893// 構築の作法は ai_content_generation_function.php に揃える(PHP はクラス外の定数定義に集約し、クラス内では JS 定数のみ参照)。
4894if (typeof globalThis.aiFloatingMenu !== 'function') {
4895
4896	// AIモジュールの契約状況とテキストチェック実行権限をサーバ側で判定して JS 定数へ渡す。
4897	//   契約なし          → ボタンを描画しない(コンパネメニューの「非表示」と整合)
4898	//   契約あり・権限なし → 通常表示のままクリックを無効化し、ホバー時の title ツールチップで使用不可を伝える(コンパネの span.disabled と同じ流儀)
4899	const aiFloatingMenu_moduleEnabled = false;
4900	const aiFloatingMenu_canRun = false;
4901
4902	globalThis.aiFloatingMenu = class aiFloatingMenu {
4903
4904		#shadowRoot = null;
4905		#cssPath = '/_administrator/css/default/ai_floating_menu.css';
4906		// AIテキストチェックの実行権限。false の場合はクリックでの起動を抑止し、ホバー時の title ツールチップで使用不可を伝える。
4907		#canRun = aiFloatingMenu_canRun;
4908
4909		// 編集サイトかつテキストチェック有効、さらに AI モジュール契約済みの場合のみフローティングボタンを描画する。
4910		async init() {
4911			// context_menuが定義されていない場合は、編集サイトではないと判断してメニューを表示しない
4912			if (typeof context_menu === 'undefined') {
4913				return;
4914			}
4915			// テキストチェック機能が無効化されている場合はこのメニューも表示しない
4916			if (typeof aiTextChecker !== 'function') {
4917				return;
4918			}
4919			// AIモジュールが未契約の場合はボタンを描画しない(コンパネメニューの非表示と整合)
4920			if (!aiFloatingMenu_moduleEnabled) {
4921				return;
4922			}
4923			// 二重生成防止
4924			if (document.getElementById('cmsAiFloatingHost')) {
4925				return;
4926			}
4927
4928			this.#createShadowHost();
4929			await this.#loadStyle();
4930			this.#renderButton();
4931			this.#bindEvents();
4932		}
4933
4934		#createShadowHost() {
4935			const host = document.createElement('div');
4936			host.id = 'cmsAiFloatingHost';
4937			// Shadow DOM 内の z-index は Host 外に伝播しないため、Host 自体に z-index を設定する。
4938			// ・ダイレクト編集中のグレーアウト(.cms_marking, z-index:9998)より下に潜らせて、編集中は AI 機能を無効化する。
4939			// ・各種ダイアログ(#overlay_modal=100002, .dialog=100003 等)よりも下なので、ダイアログ表示時もフローティングボタンは背面に隠れる(右下ヘルプアイコンと同じ挙動)。
4940			host.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;z-index:9997;';
4941			document.body.appendChild(host);
4942			this.#shadowRoot = host.attachShadow({ mode: 'open' });
4943		}
4944
4945		async #loadStyle() {
4946			const style = document.createElement('link');
4947			style.rel = 'stylesheet';
4948			style.href = this.#cssPath + '?t=1790968415';
4949			this.#shadowRoot.appendChild(style);
4950
4951			// CSS の読み込みに失敗してもボタン自体は描画・機能させる(見た目が崩れるだけに留め、reject で
4952			// 後続の #renderButton/#bindEvents を止めない)。#initModal のスタイル読み込みと同じ方針。
4953			await new Promise(resolve => {
4954				style.onload = resolve;
4955				style.onerror = resolve;
4956			});
4957		}
4958
4959		#renderButton() {
4960			// innerHTML += だと既存ノード(#loadStyle で挿入した <link>)が再パースで置換されるため、
4961			// <template> 経由で末尾に追加する。
4962			const template = document.createElement('template');
4963			template.innerHTML = this.#templateButton();
4964			this.#shadowRoot.appendChild(template.content);
4965		}
4966
4967		#templateButton = () => {
4968			// 権限なし時はラベル span に .disabled と title を付与(コンパネの span.disabled と同じ流儀)。クリック抑止は #bindEvents 側。
4969			const labelAttr = this.#canRun ? '' : ' class="disabled" title="この機能はご利用頂けません。"';
4970			return `
4971				<button id="cmsAiFloatingBtn" type="button">
4972					<img src="/_administrator/css/default/images/ai/logo-gogoai.svg" alt="AI">
4973					<span id="cmsAiFloatingBtnLabel"${labelAttr}>テキストチェック</span>
4974				</button>
4975			`;
4976		}
4977
4978		#bindEvents() {
4979			this.#shadowRoot.getElementById('cmsAiFloatingBtn').addEventListener('click', () => {
4980				// 権限がない場合は起動しない
4981				if (!this.#canRun) {
4982					return;
4983				}
4984				// 結果表示中のときは aiTextChecker 側で確認ダイアログを挟んでから上書き起動する。
4985				aiTextChecker.openWithConfirm();
4986			});
4987		}
4988	};
4989
4990	document.addEventListener('DOMContentLoaded', function() {
4991		new aiFloatingMenu().init();
4992	});
4993
4994}
4995var base_url = (location.protocol=='http:')? 'https://sokki.or.jp':'https://sokki.or.jp';
4996var now_url = location.host.replace('www.','');
4997/*
4998if( 'https://sokki.or.jp'.replace('http://','').replace('www.','') !== now_url ){
4999	base_url = '//'+now_url;
5000}
5001*/
5002base_url = base_url.replace(/^https?:/,'');
5003/*
5004*/
5005var fileLoadingImage2 = base_url + "/_administrator/images/loading.gif";		
5006var fileBottomNavCloseImage2 =  base_url + "/_administrator/images/closelabel.gif";
5007
5008var overlayOpacity2 = 0.8;	// controls transparency of shadow overlay
5009
5010var animate2 = true;			// toggles resizing animations
5011var resizeSpeed2 = 7;		// controls the speed of the image resizing animations (1=slowest and 10=fastest)
5012
5013var borderSize2 = 10;		//if you adjust the padding in the CSS, you will need to update this variable
5014var imageArray2 = new Array;
5015var activeImage2;
5016
5017if(animate2 == true){
5018	overlayDuration2 = 0.2;	// shadow fade in/out duration
5019	if(resizeSpeed2 > 10){ resizeSpeed2 = 10;}
5020	if(resizeSpeed2 < 1){ resizeSpeed2 = 1;}
5021	resizeDuration2 = (11 - resizeSpeed2) * 0.15;
5022} else { 
5023	overlayDuration2 = 0;
5024	resizeDuration2 = 0;
5025}
5026Object.extend(Element, {
5027	getWidth: function(element) {
5028	   	element = $(element);
5029	   	return element.offsetWidth; 
5030	},
5031	setWidth: function(element,w) {
5032	   	element = $(element);
5033    	element.style.width = w +"px";
5034	},
5035	setHeight: function(element,h) {
5036   		element = $(element);
5037    	element.style.height = h +"px";
5038	},
5039	setTop: function(element,t) {
5040	   	element = $(element);
5041    	element.style.top = t +"px";
5042	},
5043	setLeft: function(element,l) {
5044	   	element = $(element);
5045    	element.style.left = l +"px";
5046	},
5047	setSrc: function(element,src) {
5048    	element = $(element);
5049    	element.src = src; 
5050	},
5051	setHref: function(element,href) {
5052    	element = $(element);
5053    	element.href = href; 
5054	},
5055	setInnerHTML: function(element,content) {
5056		element = $(element);
5057		element.innerHTML = content;
5058	}
5059});
5060
5061// -----------------------------------------------------------------------------------
5062
5063//
5064//	Extending built-in Array object
5065//	- array.removeDuplicates()
5066//	- array.empty()
5067//
5068Array.prototype.removeDuplicates = function () {
5069    for(i = 0; i < this.length; i++){
5070        for(j = this.length-1; j>i; j--){        
5071            if(this[i][0] == this[j][0]){
5072                this.splice(j,1);
5073            }
5074        }
5075    }
5076}
5077
5078// -----------------------------------------------------------------------------------
5079
5080Array.prototype.empty = function () {
5081	for(i = 0; i <= this.length; i++){
5082		this.shift();
5083	}
5084}
5085
5086// -----------------------------------------------------------------------------------
5087
5088//
5089//	Lightbox Class Declaration
5090//	- initialize()
5091//	- start()
5092//	- changeImage()
5093//	- resizeImageContainer()
5094//	- showImage()
5095//	- updateDetails()
5096//	- updateNav()
5097//	- enableKeyboardNav()
5098//	- disableKeyboardNav()
5099//	- keyboardNavAction()
5100//	- preloadNeighborImages()
5101//	- end()
5102//
5103//	Structuring of code inspired by Scott Upton (http://www.uptonic.com/)
5104//
5105var Lightbox2 = Class.create();
5106
5107Lightbox2.prototype = {
5108	
5109	// initialize()
5110	// Constructor runs on completion of the DOM loading. Calls updateImageList and then
5111	// the function inserts html at the bottom of the page which is used to display the shadow 
5112	// overlay and the image container.
5113	//
5114	initialize: function() {	
5115		
5116		this.updateImageList();
5117
5118		// Code inserts html at the bottom of the page that looks similar to this:
5119		//
5120		//	<div id="overlay"></div>
5121		//	<div id="lightbox">
5122		//		<div id="outerImageContainer">
5123		//			<div id="imageContainer">
5124		//				<img id="lightboxImage">
5125		//				<div style="" id="hoverNav">
5126		//					<a href="#" id="prevLink"></a>
5127		//					<a href="#" id="nextLink"></a>
5128		//				</div>
5129		//				<div id="loading">
5130		//					<a href="#" id="loadingLink">
5131		//						<img src="images/loading.gif">
5132		//					</a>
5133		//				</div>
5134		//			</div>
5135		//		</div>
5136		//		<div id="imageDataContainer">
5137		//			<div id="imageData">
5138		//				<div id="imageDetails">
5139		//					<span id="caption"></span>
5140		//					<span id="numberDisplay"></span>
5141		//				</div>
5142		//				<div id="bottomNav">
5143		//					<a href="#" id="bottomNavClose">
5144		//						<img src="images/close.gif">
5145		//					</a>
5146		//				</div>
5147		//			</div>
5148		//		</div>
5149		//	</div>
5150
5151
5152		var objBody = document.getElementsByTagName("body").item(0);
5153		
5154		var objOverlay = document.createElement("div");
5155		objOverlay.setAttribute('id','overlay2');
5156		objOverlay.style.display = 'none';
5157		objOverlay.onclick = function() { myLightbox2.end(); }
5158		objBody.appendChild(objOverlay);
5159		
5160		var objLightbox2 = document.createElement("div");
5161		objLightbox2.setAttribute('id','lightbox2');
5162		objLightbox2.style.display = 'none';
5163		objLightbox2.onclick = function(e) {	// close Lightbox is user clicks shadow overlay
5164			if (!e) var e = window.event;
5165			var clickObj = Event.element(e).id;
5166			if ( clickObj == 'lightbox2') {
5167				myLightbox2.end();
5168			}
5169		};
5170		objBody.appendChild(objLightbox2);
5171			
5172		var objOuterImageContainer = document.createElement("div");
5173		objOuterImageContainer.setAttribute('id','outerImageContainer2');
5174		objLightbox2.appendChild(objOuterImageContainer);
5175
5176		// When Lightbox starts it will resize itself from 250 by 250 to the current image dimension.
5177		// If animations are turned off, it will be hidden as to prevent a flicker of a
5178		// white 250 by 250 box.
5179		if(animate2){
5180			Element.setWidth('outerImageContainer2', 250);
5181			Element.setHeight('outerImageContainer2', 250);			
5182		} else {
5183			Element.setWidth('outerImageContainer2', 1);
5184			Element.setHeight('outerImageContainer2', 1);			
5185		}
5186
5187		var objImageContainer = document.createElement("div");
5188		objImageContainer.setAttribute('id','imageContainer2');
5189		objOuterImageContainer.appendChild(objImageContainer);
5190	
5191		var objLightboxImage = document.createElement("img");
5192		objLightboxImage.setAttribute('id','lightboxImage2');
5193		objImageContainer.appendChild(objLightboxImage);
5194	
5195		var objHoverNav = document.createElement("div");
5196		objHoverNav.setAttribute('id','hoverNav2');
5197		objImageContainer.appendChild(objHoverNav);
5198	
5199		var objPrevLink = document.createElement("a");
5200		objPrevLink.setAttribute('id','prevLink2');
5201		objPrevLink.setAttribute('href','#');
5202		objHoverNav.appendChild(objPrevLink);
5203		
5204		var objNextLink = document.createElement("a");
5205		objNextLink.setAttribute('id','nextLink2');
5206		objNextLink.setAttribute('href','#');
5207		objHoverNav.appendChild(objNextLink);
5208	
5209		var objLoading = document.createElement("div");
5210		objLoading.setAttribute('id','loading2');
5211		objImageContainer.appendChild(objLoading);
5212	
5213		var objLoadingLink = document.createElement("a");
5214		objLoadingLink.setAttribute('id','loadingLink2');
5215		objLoadingLink.setAttribute('href','#');
5216		objLoadingLink.onclick = function() { myLightbox2.end(); return false; }
5217		objLoading.appendChild(objLoadingLink);
5218	
5219		var objLoadingImage = document.createElement("img");
5220		objLoadingImage.setAttribute('src', fileLoadingImage2);
5221		objLoadingLink.appendChild(objLoadingImage);
5222
5223		var objImageDataContainer = document.createElement("div");
5224		objImageDataContainer.setAttribute('id','imageDataContainer2');
5225		objLightbox2.appendChild(objImageDataContainer);
5226
5227		var objImageData = document.createElement("div");
5228		objImageData.setAttribute('id','imageData2');
5229		objImageDataContainer.appendChild(objImageData);
5230	
5231		var objImageDetails = document.createElement("div");
5232		objImageDetails.setAttribute('id','imageDetails2');
5233		objImageData.appendChild(objImageDetails);
5234	
5235		var objCaption = document.createElement("span");
5236		objCaption.setAttribute('id','caption2');
5237		objImageDetails.appendChild(objCaption);
5238	
5239		var objNumberDisplay = document.createElement("span");
5240		objNumberDisplay.setAttribute('id','numberDisplay2');
5241		objImageDetails.appendChild(objNumberDisplay);
5242		
5243		var objBottomNav = document.createElement("div");
5244		objBottomNav.setAttribute('id','bottomNav2');
5245		objImageData.appendChild(objBottomNav);
5246	
5247		var objBottomNavCloseLink = document.createElement("a");
5248		objBottomNavCloseLink.setAttribute('id','bottomNavClose2');
5249		objBottomNavCloseLink.setAttribute('href','#');
5250		objBottomNavCloseLink.onclick = function() { myLightbox2.end(); return false; }
5251		objBottomNav.appendChild(objBottomNavCloseLink);
5252	
5253		var objBottomNavCloseImage = document.createElement("img");
5254		objBottomNavCloseImage.setAttribute('src', fileBottomNavCloseImage2);
5255		objBottomNavCloseLink.appendChild(objBottomNavCloseImage);
5256	},
5257
5258
5259	//
5260	// updateImageList()
5261	// Loops through anchor tags looking for 'lightbox' references and applies onclick
5262	// events to appropriate links. You can rerun after dynamically adding images w/ajax.
5263	//
5264	updateImageList: function() {	
5265		if (!document.getElementsByTagName){ return; }
5266		var anchors = document.getElementsByTagName('a');
5267		var areas = document.getElementsByTagName('area');
5268
5269		// loop through all anchor tags
5270		for (var i=0; i<anchors.length; i++){
5271			var anchor = anchors[i];
5272			
5273			var relAttribute = String(anchor.getAttribute('rel'));
5274			
5275			// use the string.match() method to catch 'lightbox' references in the rel attribute
5276			if (anchor.getAttribute('href') && (relAttribute.toLowerCase().match('^(?=.*lightbox)(?!.*lightbox_).*$'))){
5277				anchor.onclick = function () {myLightbox2.start(this); return false;}
5278			}
5279		}
5280
5281		// loop through all area tags
5282		// todo: combine anchor & area tag loops
5283		for (var i=0; i< areas.length; i++){
5284			var area = areas[i];
5285			
5286			var relAttribute = String(area.getAttribute('rel'));
5287			
5288			// use the string.match() method to catch 'lightbox' references in the rel attribute
5289			if (area.getAttribute('href') && (relAttribute.toLowerCase().match('^(?=.*lightbox)(?!.*lightbox_).*$'))){
5290				area.onclick = function () {myLightbox2.start(this); return false;}
5291			}
5292		}
5293	},
5294	
5295	
5296	//
5297	//	start()
5298	//	Display overlay and lightbox. If image is part of a set, add siblings to imageArray2.
5299	//
5300	start: function(imageLink) {	
5301
5302		hideSelectBoxes();
5303		hideFlash();
5304
5305		// stretch overlay to fill page and fade in
5306		var arrayPageSize = getPageSize();
5307		Element.setWidth('overlay2', arrayPageSize[0]);
5308		Element.setHeight('overlay2', arrayPageSize[1]);
5309
5310		new Effect.Appear('overlay2', { duration: overlayDuration2, from: 0.0, to: overlayOpacity2 });
5311
5312		imageArray2 = [];
5313		imageNum = 0;		
5314
5315		if (!document.getElementsByTagName){ return; }
5316		var anchors = document.getElementsByTagName( imageLink.tagName);
5317
5318		// if image is NOT part of a set..
5319		if((imageLink.getAttribute('rel') == 'lightbox2')){
5320			// add single image to imageArray2
5321			imageArray2.push(new Array(imageLink.getAttribute('href'), imageLink.getAttribute('title')));			
5322		} else {
5323		// if image is part of a set..
5324			// loop through anchors, find other images in set, and add them to imageArray2
5325			for (var i=0; i<anchors.length; i++){
5326				var anchor = anchors[i];
5327				if (anchor.getAttribute('href') && (anchor.getAttribute('rel') == imageLink.getAttribute('rel'))){
5328					imageArray2.push(new Array(anchor.getAttribute('href'), anchor.getAttribute('title')));
5329				}
5330			}
5331			imageArray2.removeDuplicates();
5332			while(imageArray2[imageNum][0] != imageLink.getAttribute('href')) { imageNum++;}
5333		}
5334
5335		// calculate top and left offset for the lightbox 
5336		var arrayPageScroll = getPageScroll();
5337		var lightboxTop = arrayPageScroll[1] + (arrayPageSize[3] / 10);
5338		var lightboxLeft = arrayPageScroll[0];
5339		Element.setTop('lightbox2', lightboxTop);
5340		Element.setLeft('lightbox2', lightboxLeft);
5341		
5342		Element.show('lightbox2');
5343		
5344		this.changeImage(imageNum);
5345	},
5346
5347	//
5348	//	changeImage()
5349	//	Hide most elements and preload image in preparation for resizing image container.
5350	//
5351	changeImage: function(imageNum) {	
5352		
5353		activeImage2 = imageNum;	// update global var
5354
5355		// hide elements during transition
5356		if(animate2){ Element.show('loading2');}
5357		Element.hide('lightboxImage2');
5358		Element.hide('hoverNav2');
5359		Element.hide('prevLink2');
5360		Element.hide('nextLink2');
5361		Element.hide('imageDataContainer2');
5362		Element.hide('numberDisplay2');		
5363		
5364		imgPreloader = new Image();
5365		
5366		// once image is preloaded, resize image container
5367		imgPreloader.onload=function(){
5368			Element.setSrc('lightboxImage2', imageArray2[activeImage2][0]);
5369			myLightbox2.resizeImageContainer(imgPreloader.width, imgPreloader.height);
5370			
5371			imgPreloader.onload=function(){};	//	clear onLoad, IE behaves irratically with animate2d gifs otherwise 
5372		}
5373		imgPreloader.src = imageArray2[activeImage2][0];
5374	},
5375
5376	//
5377	//	resizeImageContainer()
5378	//
5379	resizeImageContainer: function( imgWidth, imgHeight) {
5380
5381		// get curren width and height
5382		this.widthCurrent = Element.getWidth('outerImageContainer2');
5383		this.heightCurrent = Element.getHeight('outerImageContainer2');
5384
5385		// get new width and height
5386		var widthNew = (imgWidth  + (borderSize2 * 2));
5387		var heightNew = (imgHeight  + (borderSize2 * 2));
5388
5389		// scalars based on change from old to new
5390		this.xScale = ( widthNew / this.widthCurrent) * 100;
5391		this.yScale = ( heightNew / this.heightCurrent) * 100;
5392
5393		// calculate size difference between new and old image, and resize if necessary
5394		wDiff = this.widthCurrent - widthNew;
5395		hDiff = this.heightCurrent - heightNew;
5396
5397		if(!( hDiff == 0)){ new Effect.Scale('outerImageContainer2', this.yScale, {scaleX: false, duration: resizeDuration2, queue: 'front'}); }
5398		if(!( wDiff == 0)){ new Effect.Scale('outerImageContainer2', this.xScale, {scaleY: false, delay: resizeDuration2, duration: resizeDuration2}); }
5399
5400		// if new and old image are same size and no scaling transition is necessary, 
5401		// do a quick pause to prevent image flicker.
5402		if((hDiff == 0) && (wDiff == 0)){
5403			if (navigator.appVersion.indexOf("MSIE")!=-1){ pause(250); } else { pause(100);} 
5404		}
5405
5406		Element.setHeight('prevLink2', imgHeight);
5407		Element.setHeight('nextLink2', imgHeight);
5408		Element.setWidth( 'imageDataContainer2', widthNew);
5409
5410		this.showImage();
5411	},
5412	
5413	//
5414	//	showImage()
5415	//	Display image and begin preloading neighbors.
5416	//
5417	showImage: function(){
5418		Element.hide('loading2');
5419		new Effect.Appear('lightboxImage2', { duration: resizeDuration2, queue: 'end', afterFinish: function(){	myLightbox2.updateDetails(); } });
5420		this.preloadNeighborImages();
5421	},
5422
5423	//
5424	//	updateDetails()
5425	//	Display caption, image number, and bottom nav.
5426	//
5427	updateDetails: function() {
5428	
5429		// if caption is not null
5430		if(imageArray2[activeImage2][1]){
5431			Element.show('caption2');
5432			Element.setInnerHTML( 'caption2', imageArray2[activeImage2][1]);
5433		}
5434		
5435		// if image is part of set display 'Image x of x' 
5436		if(imageArray2.length > 1){
5437			Element.show('numberDisplay2');
5438			Element.setInnerHTML( 'numberDisplay2', "Image " + eval(activeImage2 + 1) + " of " + imageArray2.length);
5439		}
5440
5441		new Effect.Parallel(
5442			[ new Effect.SlideDown( 'imageDataContainer2', { sync: true, duration: resizeDuration2, from: 0.0, to: 1.0 }), 
5443			  new Effect.Appear('imageDataContainer2', { sync: true, duration: resizeDuration2 }) ], 
5444			{ duration: resizeDuration2, afterFinish: function() {
5445				// update overlay size and update nav
5446				var arrayPageSize = getPageSize();
5447				Element.setHeight('overlay2', arrayPageSize[1]);
5448				myLightbox2.updateNav();
5449				}
5450			} 
5451		);
5452	},
5453
5454	//
5455	//	updateNav()
5456	//	Display appropriate previous and next hover navigation.
5457	//
5458	updateNav: function() {
5459
5460		Element.show('hoverNav2');				
5461
5462		// if not first image in set, display prev image button
5463		if(activeImage2 != 0){
5464			Element.show('prevLink2');
5465			document.getElementById('prevLink2').onclick = function() {
5466				myLightbox2.changeImage(activeImage2 - 1); return false;
5467			}
5468		}
5469
5470		// if not last image in set, display next image button
5471		if(activeImage2 != (imageArray2.length - 1)){
5472			Element.show('nextLink2');
5473			document.getElementById('nextLink2').onclick = function() {
5474				myLightbox2.changeImage(activeImage2 + 1); return false;
5475			}
5476		}
5477		
5478		this.enableKeyboardNav();
5479	},
5480
5481	//
5482	//	enableKeyboardNav()
5483	//
5484	enableKeyboardNav: function() {
5485		document.onkeydown = this.keyboardAction; 
5486	},
5487
5488	//
5489	//	disableKeyboardNav()
5490	//
5491	disableKeyboardNav: function() {
5492		document.onkeydown = '';
5493	},
5494
5495	//
5496	//	keyboardAction()
5497	//
5498	keyboardAction: function(e) {
5499		if (e == null) { // ie
5500			keycode = event.keyCode;
5501			escapeKey = 27;
5502		} else { // mozilla
5503			keycode = e.keyCode;
5504			escapeKey = e.DOM_VK_ESCAPE;
5505		}
5506
5507		key = String.fromCharCode(keycode).toLowerCase();
5508		
5509		if((key == 'x') || (key == 'o') || (key == 'c') || (keycode == escapeKey)){	// close lightbox
5510			myLightbox2.end();
5511		} else if((key == 'p') || (keycode == 37)){	// display previous image
5512			if(activeImage2 != 0){
5513				myLightbox2.disableKeyboardNav();
5514				myLightbox2.changeImage(activeImage2 - 1);
5515			}
5516		} else if((key == 'n') || (keycode == 39)){	// display next image
5517			if(activeImage2 != (imageArray2.length - 1)){
5518				myLightbox2.disableKeyboardNav();
5519				myLightbox2.changeImage(activeImage2 + 1);
5520			}
5521		}
5522
5523	},
5524
5525	//
5526	//	preloadNeighborImages()
5527	//	Preload previous and next images.
5528	//
5529	preloadNeighborImages: function(){
5530
5531		if((imageArray2.length - 1) > activeImage2){
5532			preloadNextImage = new Image();
5533			preloadNextImage.src = imageArray2[activeImage2 + 1][0];
5534		}
5535		if(activeImage2 > 0){
5536			preloadPrevImage = new Image();
5537			preloadPrevImage.src = imageArray2[activeImage2 - 1][0];
5538		}
5539	
5540	},
5541
5542	//
5543	//	end()
5544	//
5545	end: function() {
5546		this.disableKeyboardNav();
5547		Element.hide('lightbox2');
5548		new Effect.Fade('overlay2', { duration: overlayDuration2});
5549		showSelectBoxes();
5550		showFlash();
5551	}
5552}
5553
5554// -----------------------------------------------------------------------------------
5555
5556//
5557// getPageScroll()
5558// Returns array with x,y page scroll values.
5559// Core code from - quirksmode.com
5560//
5561function getPageScroll(){
5562
5563	var xScroll, yScroll;
5564
5565	if (self.pageYOffset) {
5566		yScroll = self.pageYOffset;
5567		xScroll = self.pageXOffset;
5568	} else if (document.documentElement && document.documentElement.scrollTop){	 // Explorer 6 Strict
5569		yScroll = document.documentElement.scrollTop;
5570		xScroll = document.documentElement.scrollLeft;
5571	} else if (document.body) {// all other Explorers
5572		yScroll = document.body.scrollTop;
5573		xScroll = document.body.scrollLeft;	
5574	}
5575
5576	arrayPageScroll = new Array(xScroll,yScroll) 
5577	return arrayPageScroll;
5578}
5579
5580// -----------------------------------------------------------------------------------
5581
5582//
5583// getPageSize()
5584// Returns array with page width, height and window width, height
5585// Core code from - quirksmode.com
5586// Edit for Firefox by pHaez
5587//
5588function getPageSize(){
5589	
5590	var xScroll, yScroll;
5591	
5592	if (window.innerHeight && window.scrollMaxY) {	
5593		xScroll = window.innerWidth + window.scrollMaxX;
5594		yScroll = window.innerHeight + window.scrollMaxY;
5595	} else if (document.body.scrollHeight > document.body.offsetHeight){ // all but Explorer Mac
5596		xScroll = document.body.scrollWidth;
5597		yScroll = document.body.scrollHeight;
5598	} else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari
5599		xScroll = document.body.offsetWidth;
5600		yScroll = document.body.offsetHeight;
5601	}
5602	
5603	var windowWidth, windowHeight;
5604	
5605//	console.log(self.innerWidth);
5606//	console.log(document.documentElement.clientWidth);
5607
5608	if (self.innerHeight) {	// all except Explorer
5609		if(document.documentElement.clientWidth){
5610			windowWidth = document.documentElement.clientWidth; 
5611		} else {
5612			windowWidth = self.innerWidth;
5613		}
5614		windowHeight = self.innerHeight;
5615	} else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode
5616		windowWidth = document.documentElement.clientWidth;
5617		windowHeight = document.documentElement.clientHeight;
5618	} else if (document.body) { // other Explorers
5619		windowWidth = document.body.clientWidth;
5620		windowHeight = document.body.clientHeight;
5621	}	
5622	
5623	// for small pages with total height less then height of the viewport
5624	if(yScroll < windowHeight){
5625		pageHeight = windowHeight;
5626	} else { 
5627		pageHeight = yScroll;
5628	}
5629
5630//	console.log("xScroll " + xScroll)
5631//	console.log("windowWidth " + windowWidth)
5632
5633	// for small pages with total width less then width of the viewport
5634	if(xScroll < windowWidth){	
5635		pageWidth = xScroll;		
5636	} else {
5637		pageWidth = windowWidth;
5638	}
5639//	console.log("pageWidth " + pageWidth)
5640
5641	arrayPageSize = new Array(pageWidth,pageHeight,windowWidth,windowHeight) 
5642	return arrayPageSize;
5643}
5644
5645// -----------------------------------------------------------------------------------
5646
5647//
5648// getKey(key)
5649// Gets keycode. If 'x' is pressed then it hides the lightbox.
5650//
5651function getKey(e){
5652	if (e == null) { // ie
5653		keycode = event.keyCode;
5654	} else { // mozilla
5655		keycode = e.which;
5656	}
5657	key = String.fromCharCode(keycode).toLowerCase();
5658	
5659	if(key == 'x'){
5660	}
5661}
5662
5663// -----------------------------------------------------------------------------------
5664
5665//
5666// listenKey()
5667//
5668function listenKey () {	document.onkeypress = getKey; }
5669	
5670// ---------------------------------------------------
5671
5672function showSelectBoxes(){
5673	var selects = document.getElementsByTagName("select");
5674	for (i = 0; i != selects.length; i++) {
5675		selects[i].style.visibility = "visible";
5676	}
5677}
5678
5679// ---------------------------------------------------
5680
5681function hideSelectBoxes(){
5682	var selects = document.getElementsByTagName("select");
5683	for (i = 0; i != selects.length; i++) {
5684		selects[i].style.visibility = "hidden";
5685	}
5686}
5687
5688// ---------------------------------------------------
5689
5690function showFlash(){
5691	var flashObjects = document.getElementsByTagName("object");
5692	for (i = 0; i < flashObjects.length; i++) {
5693		flashObjects[i].style.visibility = "visible";
5694	}
5695
5696	var flashEmbeds = document.getElementsByTagName("embed");
5697	for (i = 0; i < flashEmbeds.length; i++) {
5698		flashEmbeds[i].style.visibility = "visible";
5699	}
5700}
5701
5702// ---------------------------------------------------
5703
5704function hideFlash(){
5705	var flashObjects = document.getElementsByTagName("object");
5706	for (i = 0; i < flashObjects.length; i++) {
5707		flashObjects[i].style.visibility = "hidden";
5708	}
5709
5710	var flashEmbeds = document.getElementsByTagName("embed");
5711	for (i = 0; i < flashEmbeds.length; i++) {
5712		flashEmbeds[i].style.visibility = "hidden";
5713	}
5714
5715}
5716
5717
5718// ---------------------------------------------------
5719
5720//
5721// pause(numberMillis)
5722// Pauses code execution for specified time. Uses busy code, not good.
5723// Help from Ran Bar-On [[email protected]]
5724//
5725
5726function pause(ms){
5727	var date = new Date();
5728	curDate = null;
5729	do{var curDate = new Date();}
5730	while( curDate - date < ms);
5731}
5732/*
5733function pause(numberMillis) {
5734	var curently = new Date().getTime() + sender;
5735	while (new Date().getTime();	
5736}
5737*/
5738// ---------------------------------------------------
5739
5740
5741
5742function initLightbox2() { myLightbox2 = new Lightbox2(); }
5743Event.observe(window, 'load', initLightbox2, false);/* 2021-10-07 lightbox_2 _templates/_modules */
5744/*!
5745 * Lightbox v2.11.2
5746 * by Lokesh Dhakar
5747 *
5748 * More info:
5749 * http://lokeshdhakar.com/projects/lightbox2/
5750 *
5751 * Copyright Lokesh Dhakar
5752 * Released under the MIT license
5753 * https://github.com/lokesh/lightbox2/blob/master/LICENSE
5754 *
5755 * @preserve
5756 */
5757
5758// Uses Node, AMD or browser globals to create a module.
5759(function (root, factory) {
5760    if (typeof define === 'function' && define.amd) {
5761        // AMD. Register as an anonymous module.
5762        define(['jquery'], factory);
5763    } else if (typeof exports === 'object') {
5764        // Node. Does not work with strict CommonJS, but
5765        // only CommonJS-like environments that support module.exports,
5766        // like Node.
5767        module.exports = factory(require('jquery'));
5768    } else {
5769        // Browser globals (root is window)
5770        root.lightbox = factory(root.jQuery);
5771    }
5772}(this, function ($) {
5773
5774  function Lightbox(options) {
5775    this.album = [];
5776    this.currentImageIndex = void 0;
5777    this.init();
5778
5779    // options
5780    this.options = $.extend({}, this.constructor.defaults);
5781    this.option(options);
5782  }
5783
5784  // Descriptions of all options available on the demo site:
5785  // http://lokeshdhakar.com/projects/lightbox2/index.html#options
5786  Lightbox.defaults = {
5787    albumLabel: 'Image %1 of %2',
5788    alwaysShowNavOnTouchDevices: false,
5789    fadeDuration: 600,
vendor: 4,651 bytes, lines 5790-5895
5790    fitImagesInViewport: true,
5791    imageFadeDuration: 600,
5792    // maxWidth: 800,
5793    // maxHeight: 600,
5794    positionFromTop: 50,
5795    resizeDuration: 700,
5796    showImageNumberLabel: true,
5797    wrapAround: false,
5798    disableScrolling: false,
5799    /*
5800    Sanitize Title
5801    If the caption data is trusted, for example you are hardcoding it in, then leave this to false.
5802    This will free you to add html tags, such as links, in the caption.
5803
5804    If the caption data is user submitted or from some other untrusted source, then set this to true
5805    to prevent xss and other injection attacks.
5806     */
5807    sanitizeTitle: false
5808  };
5809
5810  Lightbox.prototype.option = function(options) {
5811    $.extend(this.options, options);
5812  };
5813
5814  Lightbox.prototype.imageCountLabel = function(currentImageNum, totalImages) {
5815    return this.options.albumLabel.replace(/%1/g, currentImageNum).replace(/%2/g, totalImages);
5816  };
5817
5818  Lightbox.prototype.init = function() {
5819    var self = this;
5820    // Both enable and build methods require the body tag to be in the DOM.
5821    $(document).ready(function() {
5822      self.enable();
5823      self.build();
5824    });
5825  };
5826
5827  // Loop through anchors and areamaps looking for either data-lightbox attributes or rel attributes
5828  // that contain 'lightbox'. When these are clicked, start lightbox.
5829  //Lightbox.prototype.enable = function() {
5830  //  var self = this;
5831  //  $('body').on('click', 'a[rel^=lightbox], area[rel^=lightbox], a[data-lightbox], area[data-lightbox]', function(event) {
5832  //    self.start($(event.currentTarget));
5833  //    return false;
5834  //  });
5835  //};
5836
5837  //干渉防止変更
5838  Lightbox.prototype.enable = function() {
5839    var self = this;
5840    $('body').on('click', 'a[rel^=lightbox_2], area[rel^=lightbox_2], a[data-lightbox_2], area[data-lightbox_2]', function(event) {
5841      self.start($(event.currentTarget));
5842      return false;
5843    });
5844  };
5845
5846  // Build html for the lightbox and the overlay.
5847  // Attach event handlers to the new DOM elements. click click click
5848  Lightbox.prototype.build = function() {
5849    if ($('#lightbox').length > 0) {
5850        return;
5851    }
5852
5853    var self = this;
5854
5855    // The two root notes generated, #lightboxOverlay and #lightbox are given
5856    // tabindex attrs so they are focusable. We attach our keyboard event
5857    // listeners to these two elements, and not the document. Clicking anywhere
5858    // while Lightbox is opened will keep the focus on or inside one of these
5859    // two elements.
5860    //
5861    // We do this so we can prevent propogation of the Esc keypress when
5862    // Lightbox is open. This prevents it from intefering with other components
5863    // on the page below.
5864    //
5865    // Github issue: https://github.com/lokesh/lightbox2/issues/663
5866    $('<div id="lightbox_2_Overlay" tabindex="-1" class="lightbox_2_Overlay"></div><div id="lightbox_2" tabindex="-1" class="lightbox_2"><div class="lb-outerContainer"><div class="lb-container"><img class="lb-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" alt=""/><div class="lb-nav"><a class="lb-prev" aria-label="Previous image" href="" ></a><a class="lb-next" aria-label="Next image" href="" ></a></div><div class="lb-loader"><a class="lb-cancel"></a></div></div></div><div class="lb-dataContainer"><div class="lb-data"><div class="lb-details"><span class="lb-caption"></span><span class="lb-number"></span></div><div class="lb-closeContainer"><a class="lb-close"></a></div></div></div></div>').appendTo($('body'));
5867
5868    // Cache jQuery objects
5869    this.$lightbox_2     = $('#lightbox_2');
5870    this.$overlay        = $('#lightbox_2_Overlay');
5871
5872    this.$outerContainer = this.$lightbox_2.find('.lb-outerContainer');
5873    this.$container      = this.$lightbox_2.find('.lb-container');
5874    this.$image          = this.$lightbox_2.find('.lb-image');
5875    this.$nav            = this.$lightbox_2.find('.lb-nav');
5876 
5877    // Store css values for future lookup
5878    this.containerPadding = {
5879      top: parseInt(this.$container.css('padding-top'), 10),
5880      right: parseInt(this.$container.css('padding-right'), 10),
5881      bottom: parseInt(this.$container.css('padding-bottom'), 10),
5882      left: parseInt(this.$container.css('padding-left'), 10)
5883    };
5884
5885    this.imageBorderWidth = {
5886      top: parseInt(this.$image.css('border-top-width'), 10),
5887      right: parseInt(this.$image.css('border-right-width'), 10),
5888      bottom: parseInt(this.$image.css('border-bottom-width'), 10),
5889      left: parseInt(this.$image.css('border-left-width'), 10)
5890    };
5891
5892    // Attach event handlers to the newly minted DOM elements
5893    this.$overlay.hide().on('click', function() {
5894      self.end();
5895      return false;
5896    });
5897
5898    this.$lightbox_2.hide().on('click', function(event) {
5899      if ($(event.target).attr('id') === 'lightbox') {
5900        self.end();
5901      }
5902    });
5903
5904    this.$outerContainer.on('click', function(event) {
5905      if ($(event.target).attr('id') === 'lightbox') {
5906        self.end();
5907      }
5908      return false;
5909    });
5910
5911    this.$lightbox_2.find('.lb-prev').on('click', function() {
5912      if (self.currentImageIndex === 0) {
5913        self.changeImage(self.album.length - 1);
5914      } else {
5915        self.changeImage(self.currentImageIndex - 1);
5916      }
5917      return false;
5918    });
5919
5920    this.$lightbox_2.find('.lb-next').on('click', function() {
5921      if (self.currentImageIndex === self.album.length - 1) {
5922        self.changeImage(0);
5923      } else {
5924        self.changeImage(self.currentImageIndex + 1);
5925      }
5926      return false;
5927    });
5928
5929    /*
5930      Show context menu for image on right-click
5931
5932      There is a div containing the navigation that spans the entire image and lives above of it. If
5933      you right-click, you are right clicking this div and not the image. This prevents users from
5934      saving the image or using other context menu actions with the image.
5935
5936      To fix this, when we detect the right mouse button is pressed down, but not yet clicked, we
5937      set pointer-events to none on the nav div. This is so that the upcoming right-click event on
5938      the next mouseup will bubble down to the image. Once the right-click/contextmenu event occurs
5939      we set the pointer events back to auto for the nav div so it can capture hover and left-click
5940      events as usual.
5941     */
5942    this.$nav.on('mousedown', function(event) {
5943      if (event.which === 3) {
5944        self.$nav.css('pointer-events', 'none');
5945
5946        self.$lightbox_2.one('contextmenu', function() {
5947          setTimeout(function() {
5948              this.$nav.css('pointer-events', 'auto');
5949          }.bind(self), 0);
5950        });
5951      }
5952    });
5953
5954
5955    this.$lightbox_2.find('.lb-loader, .lb-close').on('click', function() {
5956      self.end();
5957      return false;
5958    });
5959  };
5960  // Show overlay and lightbox. If the image is part of a set, add siblings to album array.
5961  Lightbox.prototype.start = function($link) {
5962    
5963    var self    = this;
5964    var $window = $(window);
5965
5966    $window.on('resize', $.proxy(this.sizeOverlay, this));
5967
5968    this.sizeOverlay();
5969
5970    this.album = [];
5971    var imageNumber = 0;
5972
5973    function addToAlbum($link) {
5974      self.album.push({
5975        alt: $link.attr('data-alt'),
5976        link: $link.attr('href'),
5977        title: $link.attr('data-title') || $link.attr('title')
5978      });
5979    }
5980
5981    // Support both data-lightbox attribute and rel attribute implementations
5982    //var dataLightboxValue = $link.attr('data-lightbox');
5983
5984    //干渉防止変更
5985    var dataLightboxValue = $link.attr('data-lightbox_2');
5986    var $links;
5987
5988    if (dataLightboxValue) {
5989      $links = $($link.prop('tagName') + '[data-lightbox="' + dataLightboxValue + '"]');
5990      for (var i = 0; i < $links.length; i = ++i) {
5991        addToAlbum($($links[i]));
5992        if ($links[i] === $link[0]) {
5993          imageNumber = i;
5994        }
5995      }
5996    } else {
5997      if ($link.attr('rel') === 'lightbox') {
5998        // If image is not part of a set
5999        addToAlbum($link);
6000      } else {
6001        // If image is part of a set
6002        $links = $($link.prop('tagName') + '[rel="' + $link.attr('rel') + '"]');        
6003        for (var j = 0; j < $links.length; j = ++j) {
6004          addToAlbum($($links[j]));
6005          if ($links[j] === $link[0]) {
6006            imageNumber = j;
6007          }
6008        }
6009      }
6010    }
6011
6012    // Position Lightbox
6013    var top  = $window.scrollTop() + this.options.positionFromTop;
6014    var left = $window.scrollLeft();
6015
6016 
6017
6018    this.$lightbox_2.css({
6019      top: top + 'px',
6020      left: left + 'px'
6021    }).fadeIn(this.options.fadeDuration);
6022
6023    // Disable scrolling of the page while open
6024    if (this.options.disableScrolling) {
6025      $('body').addClass('lb-disable-scrolling');
6026    }
6027
6028    this.changeImage(imageNumber);
6029  };
6030
6031  // Hide most UI elements in preparation for the animated resizing of the lightbox.
6032  Lightbox.prototype.changeImage = function(imageNumber) {
6033    var self = this;
6034    var filename = this.album[imageNumber].link;
6035    var filetype = filename.split('.').slice(-1)[0];
6036    var $image = this.$lightbox_2.find('.lb-image');
6037
6038    // Disable keyboard nav during transitions
6039    this.disableKeyboardNav();
6040
6041    // Show loading state
6042    this.$overlay.fadeIn(this.options.fadeDuration);
6043    $('.lb-loader').fadeIn('slow');
6044    this.$lightbox_2.find('.lb-image, .lb-nav, .lb-prev, .lb-next, .lb-dataContainer, .lb-numbers, .lb-caption').hide();
6045    this.$outerContainer.addClass('animating');
6046
6047    // When image to show is preloaded, we send the width and height to sizeContainer()
6048    var preloader = new Image();
6049    preloader.onload = function() {
6050      var $preloader;
6051      var imageHeight;
6052      var imageWidth;
6053      var maxImageHeight;
6054      var maxImageWidth;
6055      var windowHeight;
6056      var windowWidth;
6057
6058      $image.attr({
6059        'alt': self.album[imageNumber].alt,
6060        'src': filename
6061      });
6062
6063      $preloader = $(preloader);
6064
6065      $image.width(preloader.width);
6066      $image.height(preloader.height);
6067      windowWidth = $(window).width();
6068      windowHeight = $(window).height();
6069
6070      // Calculate the max image dimensions for the current viewport.
6071      // Take into account the border around the image and an additional 10px gutter on each side.
6072      maxImageWidth  = windowWidth - self.containerPadding.left - self.containerPadding.right - self.imageBorderWidth.left - self.imageBorderWidth.right - 20;
6073      maxImageHeight = windowHeight - self.containerPadding.top - self.containerPadding.bottom - self.imageBorderWidth.top - self.imageBorderWidth.bottom - self.options.positionFromTop - 70;
6074
6075      /*
6076      Since many SVGs have small intrinsic dimensions, but they support scaling
6077      up without quality loss because of their vector format, max out their
6078      size.
6079      */
6080      if (filetype === 'svg') {
6081        $image.width(maxImageWidth);
6082        $image.height(maxImageHeight);
6083      }
6084
6085      // Fit image inside the viewport.
6086      if (self.options.fitImagesInViewport) {
6087
6088        // Check if image size is larger then maxWidth|maxHeight in settings
6089        if (self.options.maxWidth && self.options.maxWidth < maxImageWidth) {
6090          maxImageWidth = self.options.maxWidth;
6091        }
6092        if (self.options.maxHeight && self.options.maxHeight < maxImageHeight) {
6093          maxImageHeight = self.options.maxHeight;
6094        }
6095
6096      } else {
6097        maxImageWidth = self.options.maxWidth || preloader.width || maxImageWidth;
6098        maxImageHeight = self.options.maxHeight || preloader.height || maxImageHeight;
6099      }
6100
6101      // Is the current image's width or height is greater than the maxImageWidth or maxImageHeight
6102      // option than we need to size down while maintaining the aspect ratio.
6103      if ((preloader.width > maxImageWidth) || (preloader.height > maxImageHeight)) {
6104        if ((preloader.width / maxImageWidth) > (preloader.height / maxImageHeight)) {
6105          imageWidth  = maxImageWidth;
6106          imageHeight = parseInt(preloader.height / (preloader.width / imageWidth), 10);
6107          $image.width(imageWidth);
6108          $image.height(imageHeight);
6109        } else {
6110          imageHeight = maxImageHeight;
6111          imageWidth = parseInt(preloader.width / (preloader.height / imageHeight), 10);
6112          $image.width(imageWidth);
6113          $image.height(imageHeight);
6114        }
6115      }
6116      self.sizeContainer($image.width(), $image.height());
6117    };
6118
6119    // Preload image before showing
6120    preloader.src = this.album[imageNumber].link;
6121    this.currentImageIndex = imageNumber;
6122  };
6123
6124  // Stretch overlay to fit the viewport
6125  Lightbox.prototype.sizeOverlay = function() {
6126    var self = this;
6127    /*
6128    We use a setTimeout 0 to pause JS execution and let the rendering catch-up.
6129    Why do this? If the `disableScrolling` option is set to true, a class is added to the body
6130    tag that disables scrolling and hides the scrollbar. We want to make sure the scrollbar is
6131    hidden before we measure the document width, as the presence of the scrollbar will affect the
6132    number.
6133    */
6134    setTimeout(function() {
6135      self.$overlay
6136        .width($(document).width())
6137        .height($(document).height());
6138
6139    }, 0);
6140  };
6141
6142  // Animate the size of the lightbox to fit the image we are showing
6143  // This method also shows the the image.
6144  Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight) {
6145    var self = this;
6146
6147    var oldWidth  = this.$outerContainer.outerWidth();
6148    var oldHeight = this.$outerContainer.outerHeight();
6149    var newWidth  = imageWidth + this.containerPadding.left + this.containerPadding.right + this.imageBorderWidth.left + this.imageBorderWidth.right;
6150    var newHeight = imageHeight + this.containerPadding.top + this.containerPadding.bottom + this.imageBorderWidth.top + this.imageBorderWidth.bottom;
6151
6152    function postResize() {
6153      self.$lightbox_2.find('.lb-dataContainer').width(newWidth);
6154      self.$lightbox_2.find('.lb-prevLink').height(newHeight);
6155      self.$lightbox_2.find('.lb-nextLink').height(newHeight);
6156
6157      // Set focus on one of the two root nodes so keyboard events are captured.
6158      self.$overlay.focus();
6159
6160      self.showImage();
6161    }
6162
6163    if (oldWidth !== newWidth || oldHeight !== newHeight) {
6164      this.$outerContainer.animate({
6165        width: newWidth,
6166        height: newHeight
6167      }, this.options.resizeDuration, 'swing', function() {
6168        postResize();
6169      });
6170    } else {
6171      postResize();
6172    }
6173  };
6174
6175  // Display the image and its details and begin preload neighboring images.
6176  Lightbox.prototype.showImage = function() {
6177    this.$lightbox_2.find('.lb-loader').stop(true).hide();
6178    this.$lightbox_2.find('.lb-image').fadeIn(this.options.imageFadeDuration);
6179
6180    this.updateNav();
6181    this.updateDetails();
6182    this.preloadNeighboringImages();
6183    this.enableKeyboardNav();
6184  };
6185
6186  // Display previous and next navigation if appropriate.
6187  Lightbox.prototype.updateNav = function() {
6188    // Check to see if the browser supports touch events. If so, we take the conservative approach
6189    // and assume that mouse hover events are not supported and always show prev/next navigation
6190    // arrows in image sets.
6191    var alwaysShowNav = false;
6192    try {
6193      document.createEvent('TouchEvent');
6194      alwaysShowNav = (this.options.alwaysShowNavOnTouchDevices) ? true : false;
6195    } catch (e) {}
6196
6197    this.$lightbox_2.find('.lb-nav').show();
6198
6199    if (this.album.length > 1) {
6200      if (this.options.wrapAround) {
6201        if (alwaysShowNav) {
6202          this.$lightbox_2.find('.lb-prev, .lb-next').css('opacity', '1');
6203        }
6204        this.$lightbox_2.find('.lb-prev, .lb-next').show();
6205      } else {
6206        if (this.currentImageIndex > 0) {
6207          this.$lightbox_2.find('.lb-prev').show();
6208          if (alwaysShowNav) {
6209            this.$lightbox_2.find('.lb-prev').css('opacity', '1');
6210          }
6211        }
6212        if (this.currentImageIndex < this.album.length - 1) {
6213          this.$lightbox_2.find('.lb-next').show();
6214          if (alwaysShowNav) {
6215            this.$lightbox_2.find('.lb-next').css('opacity', '1');
6216          }
6217        }
6218      }
6219    }
6220  };
6221
6222  // Display caption, image number, and closing button.
6223  Lightbox.prototype.updateDetails = function() {
6224    var self = this;
6225
6226    // Enable anchor clicks in the injected caption html.
6227    // Thanks Nate Wright for the fix. @https://github.com/NateWr
6228    if (typeof this.album[this.currentImageIndex].title !== 'undefined' &&
6229      this.album[this.currentImageIndex].title !== '') {
6230      var $caption = this.$lightbox_2.find('.lb-caption');
6231      if (this.options.sanitizeTitle) {
6232        $caption.text(this.album[this.currentImageIndex].title);
6233      } else {
6234        $caption.html(this.album[this.currentImageIndex].title);
6235      }
6236      $caption.fadeIn('fast');
6237    }
6238
6239    if (this.album.length > 1 && this.options.showImageNumberLabel) {
6240      var labelText = this.imageCountLabel(this.currentImageIndex + 1, this.album.length);
6241      this.$lightbox_2.find('.lb-number').text(labelText).fadeIn('fast');
6242    } else {
6243      this.$lightbox_2.find('.lb-number').hide();
6244    }
6245
6246    this.$outerContainer.removeClass('animating');
6247
6248    this.$lightbox_2.find('.lb-dataContainer').fadeIn(this.options.resizeDuration, function() {
6249      return self.sizeOverlay();
6250    });
6251  };
6252
6253  // Preload previous and next images in set.
6254  Lightbox.prototype.preloadNeighboringImages = function() {
6255    if (this.album.length > this.currentImageIndex + 1) {
6256      var preloadNext = new Image();
6257      preloadNext.src = this.album[this.currentImageIndex + 1].link;
6258    }
6259    if (this.currentImageIndex > 0) {
6260      var preloadPrev = new Image();
6261      preloadPrev.src = this.album[this.currentImageIndex - 1].link;
6262    }
6263  };
6264
6265  Lightbox.prototype.enableKeyboardNav = function() {
6266    this.$lightbox_2.on('keyup.keyboard', $.proxy(this.keyboardAction, this));
6267    this.$overlay.on('keyup.keyboard', $.proxy(this.keyboardAction, this));
6268  };
6269
6270  Lightbox.prototype.disableKeyboardNav = function() {
6271    this.$lightbox_2.off('.keyboard');
6272    this.$overlay.off('.keyboard');
6273  };
6274
6275  Lightbox.prototype.keyboardAction = function(event) {
6276    var KEYCODE_ESC        = 27;
6277    var KEYCODE_LEFTARROW  = 37;
6278    var KEYCODE_RIGHTARROW = 39;
6279
6280    var keycode = event.keyCode;
6281    if (keycode === KEYCODE_ESC) {
6282      // Prevent bubbling so as to not affect other components on the page.
6283      event.stopPropagation();
6284      this.end();
6285    } else if (keycode === KEYCODE_LEFTARROW) {
6286      if (this.currentImageIndex !== 0) {
6287        this.changeImage(this.currentImageIndex - 1);
6288      } else if (this.options.wrapAround && this.album.length > 1) {
6289        this.changeImage(this.album.length - 1);
6290      }
6291    } else if (keycode === KEYCODE_RIGHTARROW) {
6292      if (this.currentImageIndex !== this.album.length - 1) {
6293        this.changeImage(this.currentImageIndex + 1);
6294      } else if (this.options.wrapAround && this.album.length > 1) {
6295        this.changeImage(0);
6296      }
6297    }
6298  };
6299
6300  // Closing time. :-(
6301  Lightbox.prototype.end = function() {
6302    this.disableKeyboardNav();
6303    $(window).off('resize', this.sizeOverlay);
6304    this.$lightbox_2.fadeOut(this.options.fadeDuration);
6305    this.$overlay.fadeOut(this.options.fadeDuration);
6306
6307    if (this.options.disableScrolling) {
6308      $('body').removeClass('lb-disable-scrolling');
6309    }
6310  };
6311
6312  return new Lightbox();
6313}));;(function($){
6314    $(window).on('load', function(event) {
6315        const ua = navigator.userAgent;
6316        if(ua.indexOf('Trident') != -1 || ua.indexOf('MSIE') != -1) {
6317            $('.cparts-animate.box > div,.cparts-animate .box > div').css('opacity',1).css('transform','translate(0,0)');
6318            return false;
6319        } else {
6320            const options = {
6321                threshold: 0.1
6322            };
6323            const observer = new IntersectionObserver(function(entries) {
6324                for(let i in entries) {
6325                    let e = entries[i];
6326                    let target=null;
6327                    const appClass='moved';
6328                    if(e.intersectionRatio > 0 && !$(e.target).hasClass(appClass)) {
6329                        if( $(e.target).find('.box > div').length > 0 ) {
6330                            target = $(e.target).find('.box > div');
6331                        } else if( $(e.target).find('> div').length > 0 ) {
6332                            target = $(e.target).find('> div');
6333                        }
6334                        target.css('opacity',1).css('transform','none');
6335                        $(e.target).addClass(appClass);
6336                    }
6337                }
6338            },options);
6339            let elms = document.querySelectorAll('.cparts-animate');
6340            for(let i in elms) {
6341                if(elms.hasOwnProperty(i)) {
6342                    observer.observe(elms[i]);
6343                }
6344            }
6345        }
6346        // dec-ulstyle--03
6347        if($('ul.dec-ulstyle--03').length) {
6348            $('ul.dec-ulstyle--03').each(function(){
6349                $(this).find('li').addClass('dec-bg--light dec-line--primary');
6350            });
6351        }
6352  
6353        // dec-olstyle--01
6354        if($('ol.dec-olstyle--01').length) {
6355            $('ol.dec-olstyle--01').each(function(){
6356                $(this).find('li').addClass('dec-bg-pe--primary');
6357            });
6358        }
6359    });
6360}(jQuery));
6361(function($){
6362  
6363  $(window).on('load', function() {
6364
6365    $('body#type1column, body#type2column, body#type2column02, body#type3column').each(function() {
6366        // bodyのフォントサイズを抜き出す
6367        var baseSize = $('body').css('font-size')
6368        baseSize = parseInt(baseSize);
6369
6370        // フォントサイズ変更ボタンが押された時
6371        $('.cparts-header-block__fontsize').find('button').on('click', function() {
6372
6373          // 標準
6374          if ($(this).hasClass('cparts-btn-fontsize__sm')) {
6375            $('body').css('font-size', baseSize);
6376            $(this).addClass('cparts-btn-fontsize__active')
6377            $(this).siblings('button').removeClass('cparts-btn-fontsize__active')
6378
6379          // 大
6380          } else if ($(this).hasClass('cparts-btn-fontsize__md')) {
6381            $('body').css('font-size', baseSize * 1.2 + 'px');
6382            $(this).addClass('cparts-btn-fontsize__active')
6383            $(this).siblings('button').removeClass('cparts-btn-fontsize__active')
6384
6385          // 特大
6386          } else if ($(this).hasClass('cparts-btn-fontsize__xl')) {
6387            $('body').css('font-size', baseSize * 1.4 + 'px');
6388            $(this).addClass('cparts-btn-fontsize__active')
6389            $(this).siblings('button').removeClass('cparts-btn-fontsize__active')
6390          }
6391        });
6392    });
6393
6394  });
6395  
6396}(jQuery));

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.