Version 19.1 by Super Admin on 2026/03/08 23:59

Show last authors
1 {{velocity}}
2 ## 新しい学校ページ作成フォーム(学校マスターデータ検索方式)
3
4 #set($currentUser = $xcontext.user)
5 #if($currentUser == 'XWiki.XWikiGuest')
6 {{error}}学校ページの作成にはログインが必要です。[[ログイン>>path:/bin/login/XWiki/XWikiLogin]]してください。{{/error}}
7 #stop
8 #end
9
10 #if($request.action && $request.action == 'create' && $services.csrf.isTokenValid($request.form_token))
11 ## フォーム送信後の処理
12 #set($schoolCode = $request.schoolCode)
13 #set($schoolName = $request.schoolName)
14
15 ## バリデーション: 学校コードが13文字の文科省形式であること(例: D112310000377)
16 ## 形式: 学校種(C1/C2/D1/D2) + 都道府県(2桁) + 設置区分(1桁) + 学校番号(7桁) + 検査数字(1桁)
17 #if(!$schoolCode || $schoolCode.length() != 13 || !$schoolCode.matches('^[CD][12]\d{11}$'))
18 {{error}}学校コードが不正です。学校名検索から選択してください。{{/error}}
19 #stop
20 #end
21
22 #set($targetPage = "Schools.${schoolCode}.WebHome")
23
24 #if($xwiki.exists($targetPage))
25 {{error}}この学校のページは既に存在します。[[既存のページを開く>>$targetPage]]{{/error}}
26 #else
27 ## ページを作成して学校テンプレートを適用
28 $response.sendRedirect($xwiki.getURL("Schools.${schoolCode}.WebHome", 'edit',
29 "template=SeitokaiCode.SchoolTemplate&schoolCode=${schoolCode}&schoolName=${escapetool.url($schoolName)}"))
30 #end
31 #else
32
33 = 新しい学校ページを作成 =
34
35 {{info}}
36 学校名で検索してページを作成します。1校で複数のページを持つことはできません。文科省の学校コード毎に学校ページを作成することができます。
37 {{/info}}
38
39 {{html clean="false"}}
40 <div class="school-create-form">
41 <form method="post" id="createSchoolForm">
42 <input type="hidden" name="action" value="create" />
43 <input type="hidden" name="form_token" value="$services.csrf.getToken()" />
44 <input type="hidden" name="schoolCode" id="schoolCode" />
45 <input type="hidden" name="schoolName" id="schoolName" />
46
47 <div class="form-group">
48 <label class="form-label">学校を検索 <span class="required-mark">*</span></label>
49 <div class="search-input-wrap">
50 <input type="text" id="schoolSearchInput" class="form-input search-input-padded"
51 placeholder="学校名で検索(例: 日比谷、渋谷、北野...)"
52 autocomplete="off" />
53 <span class="search-input-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
54 <ul id="schoolSearchResults" class="search-results-list"></ul>
55 </div>
56 <div class="form-hint">文科省の学校マスターデータから候補を検索します。</div>
57 </div>
58
59 <!-- 選択結果 -->
60 <div id="selectedSchoolArea" style="display:none;" class="form-group">
61 <div class="selected-school-card">
62 <div class="selected-school-layout">
63 <div>
64 <div class="selected-school-label">選択された学校</div>
65 <div id="selectedSchoolName" class="selected-school-name"></div>
66 <div id="selectedSchoolInfo" class="selected-school-info"></div>
67 <div id="selectedSchoolCode" class="selected-school-code"></div>
68 </div>
69 <button type="button" onclick="clearSelection()" class="btn-change-school">変更</button>
70 </div>
71 </div>
72 </div>
73
74 <!-- 重複チェック結果 -->
75 <div id="duplicateWarning" class="duplicate-warning" style="display:none;">
76 <strong><svg class="ico" viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> この学校のページは既に存在します</strong>
77 <div style="margin-top:6px;"><a id="duplicateLink" href="#">既存のページを開く →</a></div>
78 </div>
79
80 <!-- 作成ボタン -->
81 <div id="submitArea" style="display:none;">
82 <div class="create-ready-box">
83 <svg class="ico" viewBox="0 0 24 24" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg> この学校のページはまだ作成されていません。
84 </div>
85 <button type="submit" class="btn-save btn-full-width">ページを作成</button>
86 </div>
87 </form>
88
89 <!-- 未選択時ヒント -->
90 <div id="searchHint" class="search-hint-box">
91 <div class="search-hint-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></div>
92 <div class="search-hint-text">上の検索欄から学校名を入力してください</div>
93 </div>
94 </div>
95
96 <script>
97 (function() {
98 // 学校マスターデータをJSON APIから取得
99 var schools = [];
100 var schoolsLoaded = false;
101
102 // schools.json をロード(XWiki添付ファイルとして配置)
103 // 配置先: SeitokaiCode.SchoolMasterData の添付ファイル
104 fetch('/rest/wikis/xwiki/spaces/SeitokaiCode/pages/SchoolMasterData/attachments/schools.json')
105 .then(function(r) { return r.json(); })
106 .then(function(data) { schools = data; schoolsLoaded = true; })
107 .catch(function() {
108 console.warn('学校マスターデータの読み込みに失敗しました。SeitokaiCode.SchoolMasterData に schools.json が添付されているか確認してください。');
109 });
110
111 var input = document.getElementById('schoolSearchInput');
112 var resultsList = document.getElementById('schoolSearchResults');
113 var debounceTimer = null;
114
115 input.addEventListener('input', function() {
116 clearTimeout(debounceTimer);
117 debounceTimer = setTimeout(function() { searchSchools(input.value); }, 200);
118 });
119
120 function searchSchools(query) {
121 if (!query || query.length < 1 || !schoolsLoaded) {
122 resultsList.style.display = 'none';
123 return;
124 }
125 var q = query.toLowerCase();
126 var results = schools.filter(function(s) {
127 return s.name.toLowerCase().indexOf(q) >= 0 ||
128 s.pref.indexOf(q) >= 0 ||
129 s.city.indexOf(q) >= 0 ||
130 s.code.indexOf(q) >= 0;
131 }).slice(0, 10);
132
133 resultsList.innerHTML = '';
134 if (results.length === 0) {
135 resultsList.innerHTML = '<li class="search-result-empty">該当する学校が見つかりません</li>';
136 } else {
137 results.forEach(function(s) {
138 var li = document.createElement('li');
139 li.className = 'search-result-item';
140 li.innerHTML =
141 '<div class="search-result-name">' + s.name + '</div>' +
142 '<div class="search-result-info">' +
143 s.pref + ' ' + s.city + ' ・ ' + s.type + '(' + s.est + ')' +
144 '</div>' +
145 '<div class="search-result-code">' + s.code + '</div>';
146 li.onclick = function() { selectSchool(s); };
147 resultsList.appendChild(li);
148 });
149 }
150 resultsList.style.display = 'block';
151 }
152
153 window.selectSchool = function(school) {
154 // hidden フィールドにセット
155 document.getElementById('schoolCode').value = school.code;
156 document.getElementById('schoolName').value = school.name;
157
158 // UI更新
159 input.style.display = 'none';
160 resultsList.style.display = 'none';
161 document.getElementById('searchHint').style.display = 'none';
162
163 document.getElementById('selectedSchoolName').textContent = school.name;
164 document.getElementById('selectedSchoolInfo').textContent =
165 school.pref + ' ' + school.city + ' ・ ' + school.type + '(' + school.est + ')';
166 document.getElementById('selectedSchoolCode').textContent = '学校コード: ' + school.code;
167 document.getElementById('selectedSchoolArea').style.display = 'block';
168
169 // 重複チェック(XWikiにページが存在するか確認)
170 var targetPage = 'Schools.' + school.code + '.WebHome';
171 fetch('/rest/wikis/xwiki/spaces/Schools/spaces/' + school.code + '/pages/WebHome')
172 .then(function(r) {
173 if (r.ok) {
174 // 既存ページあり
175 document.getElementById('duplicateWarning').style.display = 'block';
176 document.getElementById('duplicateLink').href =
177 '/bin/view/Schools/' + school.code + '/';
178 document.getElementById('submitArea').style.display = 'none';
179 } else {
180 // 新規作成OK
181 document.getElementById('duplicateWarning').style.display = 'none';
182 document.getElementById('submitArea').style.display = 'block';
183 }
184 })
185 .catch(function() {
186 // APIエラー時は作成を許可(サーバー側で重複チェックもある)
187 document.getElementById('submitArea').style.display = 'block';
188 });
189 };
190
191 window.clearSelection = function() {
192 document.getElementById('schoolCode').value = '';
193 document.getElementById('schoolName').value = '';
194 document.getElementById('selectedSchoolArea').style.display = 'none';
195 document.getElementById('duplicateWarning').style.display = 'none';
196 document.getElementById('submitArea').style.display = 'none';
197 document.getElementById('searchHint').style.display = '';
198 input.style.display = '';
199 input.value = '';
200 input.focus();
201 };
202
203 // キーボードナビゲーション
204 var selectedIdx = -1;
205 input.addEventListener('keydown', function(e) {
206 var items = resultsList.querySelectorAll('li');
207 if (e.key === 'ArrowDown') {
208 e.preventDefault();
209 selectedIdx = Math.min(selectedIdx + 1, items.length - 1);
210 items.forEach(function(li, i) { li.classList.toggle('active', i === selectedIdx); });
211 } else if (e.key === 'ArrowUp') {
212 e.preventDefault();
213 selectedIdx = Math.max(selectedIdx - 1, 0);
214 items.forEach(function(li, i) { li.classList.toggle('active', i === selectedIdx); });
215 } else if (e.key === 'Enter' && selectedIdx >= 0) {
216 e.preventDefault();
217 items[selectedIdx].click();
218 selectedIdx = -1;
219 } else if (e.key === 'Escape') {
220 resultsList.style.display = 'none';
221 selectedIdx = -1;
222 }
223 });
224
225 // クリック外で閉じる
226 document.addEventListener('click', function(e) {
227 if (!e.target.closest('.school-create-form')) {
228 resultsList.style.display = 'none';
229 selectedIdx = -1;
230 }
231 });
232 })();
233 </script>
234 {{/html}}
235
236 #end
237 {{/velocity}}