PageSourceSearch

https://www.ajec.or.jp/wp/wp-content/themes/ajec3/assets/js/forms-enhance.js?ver=1789706769

js ajec.or.jp collected 2026-10-02 19:07:40 UTC 34,403 bytes, 711 lines download raw bytes

1/**
2 * AJEC3 Snow Monkey Forms 入力補助
3 *
4 * - 郵便番号 → 住所自動入力(Yubinbango)
5 * - メールアドレス(確認)の一致チェック
6 * - textarea の文字数カウンター
7 * - 送信ボタンの二度押し防止(ローディング表示)
8 * - 全角数字 → 半角数字の自動変換(電話番号・郵便番号)
9 * - inputmode 属性の注入(SMF はブロック属性としてサポートしないので JS で付与)
10 * - aria-required / aria-invalid の自動付与
11 * - 会員社を選ぶ欄(仕事の依頼フォーム): 選択中の表示・選ぶためのポップアップ(社名・取扱い分野で
12 *   絞り込み)・?member=ID からの初期選択・確認画面での種別ごとの表示
13 *
14 * ## 設計上の注意(過去にハマったのでメモ)
15 *
16 * SMF は async submit 後にフォーム本体を JS で差し替える。screen が
17 * input → confirm → complete と遷移する間、input/textarea 要素は
18 * 毎回新しいものに置換される。このため「初回 DOMContentLoaded で一度だけ」では
19 * 不十分で、screen 遷移ごとに再エンハンスする必要がある。
20 *
21 * ただし MutationObserver を childList+subtree で張って、その callback の
22 * 中で DOM を追加する(例: 文字数カウンター要素の append)と、自分の追加が
23 * 自分の observer を再発火させて **無限ループ** する。
24 *
25 *   対策1: 監視は `attributeFilter: ['data-screen']` に限定(childList を見ない)
26 *   対策2: 各 setup 関数は idempotent。初期化済みの要素には dataset フラグ
27 *          (例: data-ajec3-counter-init)を付けて二重適用を防ぐ
28 *   対策3: 一度バインドしたイベントリスナーを同じ要素に重複バインドしない
29 */
30(function () {
31    'use strict';
32
33    var HP_GUARD = 'ajec3Init';
34
35    // ------------------------------------------------------------------
36    // Yubinbango: 郵便番号 → 住所自動入力
37    // ------------------------------------------------------------------
38    // Yubinbango は form 要素を h-adr、郵便番号 input を p-postal-code、
39    // 住所 input を p-region/p-locality/p-street-address と認識する。
40    // SMF はそれらのクラスを付けないので JS で補う。
41    function setupYubinbango() {
42        document.querySelectorAll('.snow-monkey-form').forEach(function (form) {
43            if (form.dataset.ajec3YubinInit === '1') return;
44
45            var postcode = form.querySelector('input[name="postcode"], input[name="postal"], input[name="zip"]');
46            var address  = form.querySelector('input[name="address"]');
47            if (!postcode || !address) {
48                return;
49            }
50
51            form.dataset.ajec3YubinInit = '1';
52
53            postcode.classList.add('p-postal-code');
54            address.classList.add('p-region', 'p-locality', 'p-street-address');
55
56            if (!form.classList.contains('h-adr')) {
57                form.classList.add('h-adr');
58                var country = document.createElement('span');
59                country.className = 'p-country-name';
60                country.style.display = 'none';
61                country.textContent = 'Japan';
62                form.insertBefore(country, form.firstChild);
63            }
64
65            // Yubinbango のインスタンスは1フォームにつき1度だけ生成
66            if (typeof window.YubinBango !== 'undefined' && typeof window.YubinBango.MicroformatDom === 'function') {
67                try { new window.YubinBango.MicroformatDom(); } catch (e) { /* noop */ }
68            }
69        });
70    }
71
72    // ------------------------------------------------------------------
73    // メール一致チェック
74    // ------------------------------------------------------------------
75    function setupEmailMatch() {
76        document.querySelectorAll('.snow-monkey-form').forEach(function (form) {
77            var email   = form.querySelector('input[name="email"]');
78            var confirm = form.querySelector('input[name="email_confirm"]');
79            if (!email || !confirm) return;
80            // 同じ confirm 要素に何度もリスナーを張らない
81            if (confirm.dataset.ajec3EmailMatchInit === '1') return;
82            confirm.dataset.ajec3EmailMatchInit = '1';
83
84            var errorEl = null;
85            function ensureErrorEl() {
86                if (errorEl && errorEl.isConnected) return errorEl;
87                errorEl = document.createElement('div');
88                errorEl.className = 'smf-error-messages ajec3-email-mismatch';
89                errorEl.style.display = 'none';
90                errorEl.textContent = 'メールアドレスが一致していません';
91                var wrap = confirm.closest('.smf-item__controls') || confirm.parentNode;
92                wrap.appendChild(errorEl);
93                return errorEl;
94            }
95
96            function check() {
97                if (!confirm.value) {
98                    if (errorEl) errorEl.style.display = 'none';
99                    confirm.removeAttribute('aria-invalid');
100                    return;
101                }
102                var mismatch = email.value && email.value !== confirm.value;
103                ensureErrorEl().style.display = mismatch ? '' : 'none';
104                if (mismatch) {
105                    confirm.setAttribute('aria-invalid', 'true');
106                } else {
107                    confirm.removeAttribute('aria-invalid');
108                }
109            }
110
111            email.addEventListener('blur', check);
112            confirm.addEventListener('blur', check);
113            confirm.addEventListener('input', check);
114        });
115    }
116
117    // ------------------------------------------------------------------
118    // textarea 文字数カウンター
119    // ------------------------------------------------------------------
120    function setupTextareaCounter() {
121        document.querySelectorAll('.snow-monkey-form textarea').forEach(function (ta) {
122            if (ta.dataset.ajec3CounterInit === '1') return;
123
124            var max = parseInt(ta.getAttribute('maxlength') || '0', 10);
125            if (!max) return;
126
127            ta.dataset.ajec3CounterInit = '1';
128
129            var counter = document.createElement('div');
130            counter.className = 'ajec3-char-counter';
131            counter.setAttribute('aria-live', 'polite');
132            // 文字数カウンターは textarea の parentNode の兄弟として挿入
133            // (parentNode.appendChild だと textarea の後ろに入って妥当)
134            ta.parentNode.appendChild(counter);
135
136            function update() {
137                var len = ta.value.length;
138                counter.textContent = len + ' / ' + max + ' 文字';
139                counter.classList.toggle('is-near-limit', len >= max * 0.9);
140                counter.classList.toggle('is-over-limit', len >= max);
141            }
142            ta.addEventListener('input', update);
143            update();
144        });
145    }
146
147    // ------------------------------------------------------------------
148    // 送信ボタンのローディング/二度押し防止(イベント委任、バインド1回のみ)
149    // ------------------------------------------------------------------
150    var submitLoadingBound = false;
151    function setupSubmitLoading() {
152        if (submitLoadingBound) return;
153        submitLoadingBound = true;
154
155        document.addEventListener('click', function (e) {
156            var t = e.target;
157            if (!t || !t.matches) return;
158            if (!t.matches('.smf-button-control__control[data-action="send"]')) return;
159            if (t.disabled) return;
160
161            var original = t.textContent;
162            t.classList.add('is-loading');
163            t.textContent = '送信中…';
164            t.disabled = true;
165            // 30 秒後にフェイルセーフで復帰(送信失敗時の体感のため)
166            setTimeout(function () {
167                t.disabled = false;
168                t.classList.remove('is-loading');
169                t.textContent = original;
170            }, 30000);
171        }, true);
172    }
173
174    // ------------------------------------------------------------------
175    // 全角→半角(電話番号・郵便番号)
176    // ------------------------------------------------------------------
177    function zenToHan(str) {
178        return str
179            .replace(/[0-9]/g, function (c) {
180                return String.fromCharCode(c.charCodeAt(0) - 0xFEE0);
181            })
182            .replace(/[ー-‐―]/g, '-');
183    }
184    function setupZenkakuToHankaku() {
185        document.querySelectorAll(
186            '.snow-monkey-form input[name="tel"], ' +
187            '.snow-monkey-form input[name="phone"], ' +
188            '.snow-monkey-form input[name="fax"], ' +
189            '.snow-monkey-form input[name="postcode"], ' +
190            '.snow-monkey-form input[name="postal"], ' +
191            '.snow-monkey-form input[name="zip"]'
192        ).forEach(function (el) {
193            if (el.dataset.ajec3ZenHanInit === '1') return;
194            el.dataset.ajec3ZenHanInit = '1';
195
196            el.addEventListener('blur', function () {
197                var v = zenToHan(el.value);
198                if (v !== el.value) el.value = v;
199            });
200        });
201    }
202
203    // ------------------------------------------------------------------
204    // inputmode 属性の注入 + aria-required
205    // ------------------------------------------------------------------
206    var INPUTMODE_MAP = {
207        email:    'email',
208        tel:      'tel',
209        phone:    'tel',
210        fax:      'tel',
211        url:      'url',
212        website:  'url',
213        postcode: 'numeric',
214        postal:   'numeric',
215        zip:      'numeric'
216    };
217    function enhanceInputAttributes() {
218        document.querySelectorAll('.snow-monkey-form input, .snow-monkey-form textarea').forEach(function (el) {
219            var name = el.getAttribute('name');
220            if (INPUTMODE_MAP[name] && !el.hasAttribute('inputmode')) {
221                el.setAttribute('inputmode', INPUTMODE_MAP[name]);
222            }
223            var validations = el.getAttribute('data-validations') || '';
224            if (validations.indexOf('required') !== -1 && !el.hasAttribute('aria-required')) {
225                el.setAttribute('aria-required', 'true');
226            }
227        });
228    }
229
230    // ------------------------------------------------------------------
231    // 会員社を選ぶ欄(仕事の依頼フォーム。選択肢は inc/form-members.php が会員の投稿から作る)
232    // ------------------------------------------------------------------
233    // SMF が描いた会員種別ごとのチェックボックスは画面では隠し(送信と確認画面にはそのまま使う)、
234    // 代わりに「選択中の会社(×で外せる)」と「一覧から選ぶ」ボタンを出す。ボタンで開くポップアップ
235    // (<dialog>。スマホは全画面)で、社名・取扱い分野で絞り込みながら選ぶ。ポップアップのチェックは
236    // その場で SMF のチェックボックスに入れる(送信されるのは SMF のほう)。
237    //   - ?member=ID(会員社の詳細ページのボタン)からの初期チェック。ページを開いた最初の1回だけ
238    //   - 確認画面: 選ばれた会社を種別ごとに並べ、何も選ばれていなければ「指定なし」
239    //   - <dialog> が使えない・途中で失敗したときは、html の ajec3-members-js クラスを外して
240    //     SMF のチェックボックス一覧に戻す(クラスは inc/form-members.php が head で付ける)
241    // SMF は画面が変わるたびに .smf-placeholder の中身だけを入れ替えるので、足した部品は placeholder の外に置く。
242    // 取扱い分野・得意分野・紹介ページの URL は window.ajec3FormMembers(inc/form-members.php)。
243    var memberPreselectDone = false;
244    var memberUid = 0;
245    var MEMBER_ICON_CLOSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>';
246    var MEMBER_ICON_EXT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 4h6v6"/><path d="M20 4l-9 9"/><path d="M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/></svg>';
247
248    function memberEl(tag, className, text) {
249        var el = document.createElement(tag);
250        if (className) el.className = className;
251        if (text) el.textContent = text;
252        return el;
253    }
254
255    function normalizeMemberName(s) {
256        s = String(s || '');
257        if (s.normalize) s = s.normalize('NFKC');
258        return s.toLowerCase().replace(/\s+/g, '');
259    }
260
261    function memberData() {
262        var d = window.ajec3FormMembers;
263        return {
264            members: (d && d.members) || {},
265            tags: (d && d.tags) || []
266        };
267    }
268
269    function memberBoxes(item) {
270        return [].slice.call(item.querySelectorAll('.smf-placeholder input[type="checkbox"]'));
271    }
272
273    function memberBox(item, value) {
274        value = String(value || '').trim();
275        if (!/^\d+$/.test(value)) return null;
276        return item.querySelector('.smf-placeholder input[type="checkbox"][value="' + value + '"]');
277    }
278
279    function memberCount(item) {
280        return memberBoxes(item).filter(function (box) { return box.checked; }).length;
281    }
282
283    // ポップアップが使えないとき・失敗したときは SMF のチェックボックス一覧に戻す
284    function memberFallback() {
285        document.documentElement.classList.remove('ajec3-members-js');
286    }
287
288    // フォーム側: 選択中の会社(チップ)と件数
289    function updateMemberSummary(item, ui) {
290        var checked = memberBoxes(item).filter(function (box) { return box.checked; });
291        ui.chips.textContent = '';
292        checked.forEach(function (box) {
293            var label = box.closest('label');
294            var name = label ? label.textContent.trim() : box.value;
295            var ph = box.closest('.smf-placeholder');
296            var legend = ph ? ph.querySelector('legend') : null;
297            var chip = memberEl('li', 'ajec3-members__chip');
298            var remove = memberEl('button', 'ajec3-members__chip-remove');
299            remove.type = 'button';
300            remove.innerHTML = MEMBER_ICON_CLOSE;
301            remove.setAttribute('data-member-value', box.value);
302            remove.setAttribute('aria-label', name + 'を外す');
303            chip.appendChild(memberEl('span', 'ajec3-members__chip-name', name));
304            if (legend) chip.appendChild(memberEl('span', 'ajec3-members__chip-group', legend.textContent.trim()));
305            chip.appendChild(remove);
306            ui.chips.appendChild(chip);
307        });
308        ui.chips.hidden = !checked.length;
309        ui.status.textContent = checked.length ? '選択中 ' + checked.length + '社' : 'まだ選ばれていません';
310        ui.status.classList.toggle('is-none', !checked.length);
311        ui.open.lastChild.textContent = checked.length ? '追加・変更する' : '一覧から選ぶ';
312    }
313
314    // ポップアップ: 件数と「すべて外す」
315    function updateMemberDialogCount(item, ui) {
316        var count = memberCount(item);
317        ui.dialog.count.textContent = count ? '選択中 ' + count + '社' : 'まだ選ばれていません';
318        ui.dialog.count.classList.toggle('is-none', !count);
319        ui.dialog.clear.hidden = !count;
320    }
321
322    // ポップアップの一覧を、いまの SMF のチェックボックスから作り直す(修正・入力エラーで描き直されても合う)
323    function renderMemberRows(item, ui) {
324        var data = memberData();
325        var tagNames = {};
326        data.tags.forEach(function (t) { tagNames[t.slug] = t.name; });
327        var groups = ui.dialog.groups;
328        groups.textContent = '';
329
330        [].slice.call(item.querySelectorAll('.smf-placeholder')).forEach(function (ph) {
331            var boxes = [].slice.call(ph.querySelectorAll('input[type="checkbox"]'));
332            if (!boxes.length) return;
333            var legend = ph.querySelector('legend');
334            var section = memberEl('section', 'ajec3-members-dialog__group');
335            var heading = memberEl('h3', 'ajec3-members-dialog__group-title', legend ? legend.textContent.trim() : '');
336            heading.appendChild(memberEl('span', 'ajec3-members-dialog__group-count', boxes.length + '社'));
337            var list = memberEl('ul', 'ajec3-members-dialog__list');
338
339            boxes.forEach(function (box) {
340                var info = data.members[box.value] || {};
341                var label = box.closest('label');
342                var name = label ? label.textContent.trim() : box.value;
343                var row = memberEl('li', 'ajec3-members-dialog__row');
344                row.setAttribute('data-value', box.value);
345                row.setAttribute('data-name', normalizeMemberName(name));
346                row.setAttribute('data-tags', (info.tags || []).join(' '));
347                row.classList.toggle('is-checked', box.checked);
348
349                var pick = memberEl('label', 'ajec3-members-dialog__pick');
350                var check = memberEl('input', 'ajec3-members-dialog__check');
351                check.type = 'checkbox';
352                check.value = box.value;
353                check.checked = box.checked;
354                var text = memberEl('span', 'ajec3-members-dialog__text');
355                text.appendChild(memberEl('span', 'ajec3-members-dialog__name', name));
356                var specialty = data.tags.filter(function (t) { return (info.specialty || []).indexOf(t.slug) !== -1; });
357                if (specialty.length) {
358                    var specs = memberEl('span', 'ajec3-members-dialog__specs');
359                    specs.appendChild(memberEl('span', 'visually-hidden', '得意分野:'));
360                    specialty.forEach(function (t) {
361                        var spec = memberEl('span', 'ajec3-members-dialog__spec', tagNames[t.slug]);
362                        spec.setAttribute('data-slug', t.slug);
363                        specs.appendChild(spec);
364                    });
365                    text.appendChild(specs);
366                }
367                pick.appendChild(check);
368                pick.appendChild(text);
369                row.appendChild(pick);
370
371                if (info.url) {
372                    var more = memberEl('a', 'ajec3-members-dialog__more');
373                    more.href = info.url;
374                    more.target = '_blank';
375                    more.rel = 'noopener';
376                    more.setAttribute('aria-label', name + 'の紹介ページ(新しいタブで開きます)');
377                    more.innerHTML = '<span aria-hidden="true">紹介</span>' + MEMBER_ICON_EXT;
378                    row.appendChild(more);
379                }
380                list.appendChild(row);
381            });
382
383            section.appendChild(heading);
384            section.appendChild(list);
385            groups.appendChild(section);
386        });
387    }
388
389    // ポップアップ: 社名(部分一致)と取扱い分野(選んだどれかを扱う会社。会員一覧の絞り込みと同じ)
390    function filterMemberRows(ui) {
391        var q = normalizeMemberName(ui.dialog.search.value);
392        var tags = [].slice.call(ui.dialog.tags.querySelectorAll('[aria-pressed="true"]')).map(function (b) {
393            return b.getAttribute('data-tag');
394        });
395        var any = false;
396        [].slice.call(ui.dialog.groups.querySelectorAll('.ajec3-members-dialog__group')).forEach(function (section) {
397            var visible = 0;
398            [].slice.call(section.querySelectorAll('.ajec3-members-dialog__row')).forEach(function (row) {
399                var rowTags = (row.getAttribute('data-tags') || '').split(' ');
400                var hit = (!q || row.getAttribute('data-name').indexOf(q) !== -1) &&
401                    (!tags.length || tags.some(function (t) { return rowTags.indexOf(t) !== -1; }));
402                row.hidden = !hit;
403                if (hit) visible++;
404            });
405            section.hidden = !visible;
406            if (visible) any = true;
407        });
408        ui.dialog.empty.hidden = any;
409    }
410
411    function buildMemberDialog(item, ui) {
412        var id = 'ajec3-members-dialog-' + (++memberUid);
413        var d = {
414            el:     memberEl('dialog', 'ajec3-members-dialog'),
415            search: memberEl('input', 'ajec3-members-dialog__search'),
416            tags:   memberEl('div', 'ajec3-members-dialog__tags'),
417            body:   memberEl('div', 'ajec3-members-dialog__body'),
418            groups: memberEl('div', 'ajec3-members-dialog__groups'),
419            empty:  memberEl('p', 'ajec3-members-dialog__empty', '条件に合う会員社がありません。'),
420            count:  memberEl('p', 'ajec3-members-dialog__count'),
421            clear:  memberEl('button', 'ajec3-members-dialog__clear', 'すべて外す'),
422            done:   memberEl('button', 'ajec3-members-dialog__done', '決定する')
423        };
424        d.el.setAttribute('aria-labelledby', id + '-title');
425
426        var head = memberEl('div', 'ajec3-members-dialog__head');
427        var headText = memberEl('div', 'ajec3-members-dialog__head-text');
428        var title = memberEl('h2', 'ajec3-members-dialog__title', '依頼したい会員社を選ぶ');
429        title.id = id + '-title';
430        headText.appendChild(title);
431        headText.appendChild(memberEl('p', 'ajec3-members-dialog__lead', '複数選べます。社名や取扱い分野で絞り込めます。'));
432        var close = memberEl('button', 'ajec3-members-dialog__close');
433        close.type = 'button';
434        close.setAttribute('aria-label', '閉じる');
435        close.innerHTML = MEMBER_ICON_CLOSE;
436        head.appendChild(headText);
437        head.appendChild(close);
438
439        var filters = memberEl('div', 'ajec3-members-dialog__filters');
440        d.search.type = 'search';
441        d.search.placeholder = '社名で絞り込む';
442        d.search.autocomplete = 'off';
443        d.search.setAttribute('aria-label', '会員社を社名で絞り込む');
444        filters.appendChild(d.search);
445        var data = memberData();
446        if (data.tags.length) {
447            d.tags.setAttribute('role', 'group');
448            d.tags.setAttribute('aria-label', '取扱い分野で絞り込む');
449            data.tags.forEach(function (t) {
450                var tag = memberEl('button', 'ajec3-members-dialog__tag', t.name);
451                tag.type = 'button';
452                tag.setAttribute('data-tag', t.slug);
453                tag.setAttribute('aria-pressed', 'false');
454                d.tags.appendChild(tag);
455            });
456            filters.appendChild(d.tags);
457        }
458
459        d.empty.hidden = true;
460        d.body.appendChild(d.groups);
461        d.body.appendChild(d.empty);
462
463        var foot = memberEl('div', 'ajec3-members-dialog__foot');
464        d.count.setAttribute('aria-live', 'polite');
465        d.clear.type = 'button';
466        d.done.type = 'button';
467        foot.appendChild(d.count);
468        foot.appendChild(d.clear);
469        foot.appendChild(d.done);
470
471        d.el.appendChild(head);
472        d.el.appendChild(filters);
473        d.el.appendChild(d.body);
474        d.el.appendChild(foot);
475        document.body.appendChild(d.el);
476
477        close.addEventListener('click', function () { d.el.close(); });
478        d.done.addEventListener('click', function () { d.el.close(); });
479        d.search.addEventListener('input', function () { filterMemberRows(ui); });
480        d.tags.addEventListener('click', function (e) {
481            var tag = e.target.closest ? e.target.closest('[data-tag]') : null;
482            if (!tag) return;
483            tag.setAttribute('aria-pressed', tag.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
484            filterMemberRows(ui);
485        });
486        d.groups.addEventListener('change', function (e) {
487            var check = e.target;
488            if (!check || check.type !== 'checkbox') return;
489            var box = memberBox(item, check.value);
490            if (box) box.checked = check.checked;
491            var row = check.closest('.ajec3-members-dialog__row');
492            if (row) row.classList.toggle('is-checked', check.checked);
493            updateMemberDialogCount(item, ui);
494            updateMemberSummary(item, ui);
495        });
496        d.clear.addEventListener('click', function () {
497            memberBoxes(item).forEach(function (box) { box.checked = false; });
498            [].slice.call(d.groups.querySelectorAll('.ajec3-members-dialog__check')).forEach(function (check) {
499                check.checked = false;
500                check.closest('.ajec3-members-dialog__row').classList.remove('is-checked');
501            });
502            updateMemberDialogCount(item, ui);
503            updateMemberSummary(item, ui);
504            d.search.focus();
505        });
506        // 背景(ポップアップの外)を押したら閉じる。中で押して外で離した(文字選択など)ときは閉じない
507        var downOnBackdrop = false;
508        d.el.addEventListener('pointerdown', function (e) { downOnBackdrop = e.target === d.el; });
509        d.el.addEventListener('click', function (e) {
510            if (downOnBackdrop && e.target === d.el) d.el.close();
511            downOnBackdrop = false;
512        });
513        d.el.addEventListener('close', function () {
514            document.documentElement.classList.remove('ajec3-modal-open');
515            updateMemberSummary(item, ui);
516            ui.open.focus();
517        });
518        return d;
519    }
520
521    function openMemberDialog(item, ui) {
522        if (!ui.dialog) ui.dialog = buildMemberDialog(item, ui);
523        renderMemberRows(item, ui);
524        filterMemberRows(ui);
525        updateMemberDialogCount(item, ui);
526        document.documentElement.classList.add('ajec3-modal-open');
527        ui.dialog.el.showModal();
528    }
529
530    function buildMemberPicker(item) {
531        var controls = item.querySelector('.smf-item__controls');
532        if (!controls || !memberBoxes(item).length) return null;
533        if (typeof window.HTMLDialogElement !== 'function' ||
534            typeof document.createElement('dialog').showModal !== 'function') {
535            memberFallback();
536            return null;
537        }
538
539        var ui = {
540            bar:    memberEl('div', 'ajec3-members__bar'),
541            status: memberEl('p', 'ajec3-members__status'),
542            open:   memberEl('button', 'ajec3-members__open'),
543            chips:  memberEl('ul', 'ajec3-members__chips'),
544            none:   memberEl('p', 'ajec3-members__none', '指定なし'),
545            dialog: null
546        };
547        ui.status.setAttribute('aria-live', 'polite');
548        ui.chips.setAttribute('aria-label', '選択中の会員社');
549        ui.open.type = 'button';
550        ui.open.setAttribute('aria-haspopup', 'dialog');
551        ui.open.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M9 6h11M9 12h11M9 18h11M4 6h.01M4 12h.01M4 18h.01"/></svg><span></span>';
552        ui.none.hidden = true;
553        ui.bar.appendChild(ui.status);
554        ui.bar.appendChild(ui.open);
555        ui.bar.appendChild(ui.chips);
556        controls.insertBefore(ui.bar, controls.firstChild);
557        controls.appendChild(ui.none);
558
559        ui.open.addEventListener('click', function () { openMemberDialog(item, ui); });
560        ui.chips.addEventListener('click', function (e) {
561            var remove = e.target.closest ? e.target.closest('[data-member-value]') : null;
562            if (!remove) return;
563            var box = memberBox(item, remove.getAttribute('data-member-value'));
564            if (box) box.checked = false;
565            updateMemberSummary(item, ui);
566            ui.open.focus(); // 押したチップは消えるので、フォーカスは「一覧から選ぶ」へ
567        });
568        return ui;
569    }
570
571    function preselectMembers(item) {
572        var raw = '';
573        try {
574            raw = new URLSearchParams(window.location.search).get('member') || '';
575        } catch (e) {
576            return;
577        }
578        raw.split(',').forEach(function (value) {
579            var box = memberBox(item, value);
580            if (box) box.checked = true;
581        });
582    }
583
584    function renderMemberConfirm(item, ui) {
585        var any = false;
586        [].slice.call(item.querySelectorAll('.smf-placeholder')).forEach(function (ph) {
587            var picked = ph.querySelector('input[type="hidden"]');
588            ph.hidden = !picked;
589            if (!picked) return;
590            any = true;
591            var box = ph.firstElementChild;
592            if (box && ph.dataset.ajec3Legend && !box.querySelector('.ajec3-members__group')) {
593                box.insertBefore(memberEl('span', 'ajec3-members__group', ph.dataset.ajec3Legend), box.firstChild);
594            }
595        });
596        ui.none.hidden = any;
597    }
598
599    function setupMemberPicker() {
600        document.querySelectorAll('.snow-monkey-form .ajec3-members-item').forEach(function (item) {
601            var form = item.closest('.snow-monkey-form');
602            // 初期チェックはポップアップが使えない(一覧のまま)ときも効かせる
603            if (!memberPreselectDone && memberBoxes(item).length) {
604                memberPreselectDone = true;
605                preselectMembers(item);
606            }
607            var ui = item.ajec3Members || (item.ajec3Members = buildMemberPicker(item));
608            if (!ui) return;
609
610            if (form && form.getAttribute('data-screen') === 'confirm') {
611                renderMemberConfirm(item, ui);
612                return;
613            }
614            if (!memberBoxes(item).length) return; // SMF の描画待ち
615
616            ui.none.hidden = true;
617            [].slice.call(item.querySelectorAll('.smf-placeholder')).forEach(function (ph) {
618                ph.hidden = false;
619                var legend = ph.querySelector('legend');
620                if (legend) ph.dataset.ajec3Legend = legend.textContent.trim();
621            });
622            updateMemberSummary(item, ui);
623        });
624    }
625
626    function runMemberPicker() {
627        try {
628            setupMemberPicker();
629        } catch (e) {
630            memberFallback();
631            if (window.console && console.error) {
632                console.error('[ajec3-forms-enhance] setupMemberPicker failed:', e);
633            }
634        }
635    }
636
637    // ------------------------------------------------------------------
638    // 画面遷移時スクロール(data-screen 属性のみ監視、childList は見ない)
639    // ------------------------------------------------------------------
640    function setupScreenObserver() {
641        document.querySelectorAll('.snow-monkey-form').forEach(function (form) {
642            if (form.dataset.ajec3ScreenObsInit === '1') return;
643            form.dataset.ajec3ScreenObsInit = '1';
644
645            var observer = new MutationObserver(function () {
646                var screen = form.getAttribute('data-screen');
647                // 画面が変わったらスクロール
648                if (screen === 'confirm' || screen === 'complete') {
649                    var rect = form.getBoundingClientRect();
650                    if (rect.top < 0) {
651                        window.scrollTo({
652                            top: rect.top + window.pageYOffset - 24,
653                            behavior: 'smooth'
654                        });
655                    }
656                }
657                // SMF が入力画面を描き直した時、差し替えられた要素に再適用する必要がある。
658                // childList を監視しないので、画面切替を契機に再エンハンスする。
659                // data-screen は SMF が返す method そのもの。最初の表示は input、
660                // 確認画面から「修正する」で戻ると back、入力エラーで戻ると invalid になる。
661                if (screen === 'input' || screen === 'back' || screen === 'invali
661d') {
662                    // 次のマイクロタスクで実行(属性更新直後に DOM 差し替え完了する場合のため)
663                    setTimeout(function () {
664                        enhanceInputAttributes();
665                        setupEmailMatch();
666                        setupTextareaCounter();
667                        setupZenkakuToHankaku();
668                        runMemberPicker();
669                    }, 0);
670                }
671                if (screen === 'confirm') {
672                    setTimeout(function () {
673                        runMemberPicker();
674                    }, 0);
675                }
676            });
677            // ⚠️ childList は絶対に監視しない。監視するとカウンター追加などで無限ループする。
678            observer.observe(form, { attributes: true, attributeFilter: ['data-screen'] });
679        });
680    }
681
682    // ------------------------------------------------------------------
683    // 起動: 各 setup を try/catch で包み、1つの失敗で全体が止まらないようにする
684    // ------------------------------------------------------------------
685    function safeRun(fn, name) {
686        try {
687            fn();
688        } catch (e) {
689            if (window.console && console.error) {
690                console.error('[ajec3-forms-enhance] ' + name + ' failed:', e);
691            }
692        }
693    }
694
695    function initAll() {
696        safeRun(setupYubinbango,        'setupYubinbango');
697        safeRun(setupEmailMatch,        'setupEmailMatch');
698        safeRun(setupTextareaCounter,   'setupTextareaCounter');
699        safeRun(setupSubmitLoading,     'setupSubmitLoading');
700        safeRun(setupZenkakuToHankaku,  'setupZenkakuToHankaku');
701        safeRun(enhanceInputAttributes, 'enhanceInputAttributes');
702        runMemberPicker();
703        safeRun(setupScreenObserver,    'setupScreenObserver');
704    }
705
706    if (document.readyState === 'loading') {
707        document.addEventListener('DOMContentLoaded', initAll);
708    } else {
709        initAll();
710    }
711})();

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.