PageSourceSearch

https://lawcityblog.com/wp-content/plugins/bunken-battle/assets/js/frontend.js?ver=3.0.0

js lawcityblog.com collected 2026-10-02 01:34:14 UTC 24,832 bytes, 465 lines download raw bytes

1/* 文系ラボ 二択バトル v2.0 - Frontend JS */
2(function ($) {
3    'use strict';
4
5    var POLL_INTERVAL = 30000; // 30秒ポーリング
6
7    document.addEventListener('DOMContentLoaded', function () {
8        document.querySelectorAll('.bb-wrap[data-theme-id]').forEach(function (wrap) {
9            var themeId = wrap.dataset.themeId;
10            initGradeSelect(themeId);
11            initVoteButtons(themeId);
12            initCommentForm(themeId);
13            initLikeButtons(wrap);
14            initReactionButtons(wrap);
15            initCountdown(wrap);
16            if (wrap.dataset.isEnded !== '1') startPolling(themeId);
17        });
18    });
19
20    /* ── CSSドーナツグラフ更新 ── */
21    function updateDonut(themeId, votesA, votesB) {
22        var donut = document.getElementById('bb-donut-' + themeId);
23        var pctEl = document.getElementById('bb-donut-pct-' + themeId);
24        var subEl = document.getElementById('bb-donut-sub-' + themeId);
25        if (!donut) return;
26        var wrap  = document.getElementById('bb-battle-' + themeId);
27        var cA    = wrap ? wrap.dataset.colorA : '#FF6B6B';
28        var cB    = wrap ? wrap.dataset.colorB : '#4ECDC4';
29        var lA    = wrap ? wrap.dataset.labelA : 'A';
30        var lB    = wrap ? wrap.dataset.labelB : 'B';
31        var total = votesA + votesB;
32        var pctA  = total > 0 ? Math.round(votesA / total * 100) : 50;
33        donut.style.background = 'conic-gradient(' + cA + ' 0% ' + pctA + '%, ' + cB + ' ' + pctA + '% 100%)';
34        if (pctEl) pctEl.textContent = Math.max(pctA, 100 - pctA) + '%';
35        if (subEl) subEl.textContent = pctA >= (100 - pctA) ? lA : lB;
36    }
37
38    /* ── 結果テキスト更新 ── */
39    function updateResultDisplay(themeId, votes) {
40        var vA = votes.a || 0, vB = votes.b || 0;
41        var total = vA + vB;
42        var pctA  = total > 0 ? Math.round(vA / total * 100) : 0;
43        setText('bb-cnt-a-' + themeId, vA);
44        setText('bb-cnt-b-' + themeId, vB);
45        setText('bb-pct-a-' + themeId, pctA + '%');
46        setText('bb-pct-b-' + themeId, (100 - pctA) + '%');
47        var totalEl = document.querySelector('#bb-battle-' + themeId + ' .bb-total-num');
48        if (totalEl) totalEl.textContent = fmt(total);
49        updateDonut(themeId, vA, vB);
50    }
51
52    /* ── 学年別集計更新 ── */
53    function updateGrades(themeId, gradeVotes) {
54        var grid = document.getElementById('bb-breakdown-' + themeId);
55        if (!grid) return;
56        ['中1','中2','中3','高1','高2','高3'].forEach(function (g) {
57            var row = grid.querySelector('[data-grade="' + g + '"]');
58            if (!row) return;
59            var gv = gradeVotes[g] || { a: 0, b: 0 };
60            var gt = gv.a + gv.b;
61            var pa = gt > 0 ? Math.round(gv.a / gt * 100) : 0;
62            var pb = 100 - pa;
63            var barA = row.querySelector('.bb-grade-bar-a');
64            var barB = row.querySelector('.bb-grade-bar-b');
65            if (barA) { barA.style.width = pa + '%'; barA.textContent = pa > 15 ? pa + '%' : ''; }
66            if (barB) { barB.style.width = pb + '%'; barB.textContent = pb > 15 ? pb + '%' : ''; }
67            var tot = row.querySelector('.bb-grade-total');
68            if (tot) tot.textContent = gt + '票';
69        });
70    }
71
72    /* ── 学年選択 ── */
73    function initGradeSelect(themeId) {
74        var cont = document.getElementById('bb-grade-' + themeId);
75        if (!cont) return;
76        cont.querySelectorAll('.bb-grade-btn').forEach(function (btn) {
77            btn.addEventListener('click', function () {
78                cont.querySelectorAll('.bb-grade-btn').forEach(function (b) { b.classList.remove('selected'); });
79                btn.classList.add('selected');
80                var err = document.getElementById('bb-grade-error-' + themeId);
81                if (err) err.classList.remove('show');
82            });
83        });
84    }
85
86    /* ── 投票ボタン ── */
87    function initVoteButtons(themeId) {
88        var area = document.getElementById('bb-vote-area-' + themeId);
89        if (!area) return;
90        area.querySelectorAll('.bb-btn').forEach(function (btn) {
91            btn.addEventListener('click', function () {
92                var grade = getSelectedGrade(themeId);
93                if (!grade) {
94                    var err = document.getElementById('bb-grade-error-' + themeId);
95                    if (err) err.classList.add('show');
96                    return;
97                }
98                area.querySelectorAll('.bb-btn').forEach(function (b) { b.disabled = true; });
99                $.ajax({
100                    url: BB_Config.ajax_url, method: 'POST',
101                    data: { action: 'bb_vote', nonce: BB_Config.nonce, theme_id: themeId, choice: btn.dataset.choice, grade: grade },
102                    success: function (res) {
103                        if (res.success) {
104                            var wrap = document.getElementById('bb-battle-' + themeId);
105                            if (wrap) wrap.dataset.myChoice = res.data.my_choice || btn.dataset.choice;
106                            showResults(themeId, res.data.votes, res.data.grade_votes);
107                            // コメントフォーム表示(未コメント前提)
108                            showCommentForm(themeId, res.data.my_choice || btn.dataset.choice, grade);
109                            if (res.data.milestone) showMilestone(res.data.milestone);
110                            // GA イベント送信
111                            bbTrack('vote', {
112                                theme_id: themeId,
113                                choice: btn.dataset.choice,
114                                grade: grade,
115                            });
116                        } else {
117                            alert(res.data ? res.data.message : '投票に失敗しました。');
118                            area.querySelectorAll('.bb-btn').forEach(function (b) { b.disabled = false; });
119                        }
120                    },
121                    error: function () {
122                        alert('通信エラーが発生しました。');
123                        area.querySelectorAll('.bb-btn').forEach(function (b) { b.disabled = false; });
124                    }
125                });
126            });
127        });
128    }
129
130    /* ── 結果エリアを表示 ── */
131    function showResults(themeId, votes, gradeVotes) {
132        var voteArea = document.getElementById('bb-vote-area-' + themeId);
133        if (voteArea) voteArea.style.display = 'none';
134        var resultArea = document.getElementById('bb-result-area-' + themeId);
135        if (resultArea) resultArea.classList.remove('bb-hidden');
136        updateResultDisplay(themeId, votes);
137        if (gradeVotes) updateGrades(themeId, gradeVotes);
138    }
139
140    /* ── コメントフォームを動的生成 ── */
141    function showCommentForm(themeId, choice, grade) {
142        var section = document.getElementById('bb-comment-section-' + themeId);
143        if (!section) return;
144        // 既存フォームやメッセージを削除
145        var old = section.querySelector('.bb-comment-form-wrap, .bb-vote-first-msg, .bb-commented-done');
146        if (old) old.remove();
147
148        var html = '<div class="bb-comment-form-wrap" id="bb-comment-form-wrap-' + themeId + '">' +
149            '<div class="bb-comment-form-title">💬 あなたはなぜこっち?理由を教えて!</div>' +
150            '<textarea class="bb-comment-input" id="bb-comment-input-' + themeId + '" placeholder="ä¾
150‹ï¼šæ¯Žæ—¥å°‘しずつやった方が焦らなくて済むから!(最大200文字)" maxlength="200" rows="3"></textarea>' +
151            '<div class="bb-comment-footer">' +
152            '<span class="bb-char-count"><span id="bb-char-' + themeId + '">0</span>/200</span>' +
153            '<button class="bb-submit-btn" id="bb-comment-submit-' + themeId + '" data-theme-id="' + themeId + '" data-choice="' + escHtml(choice) + '" data-grade="' + escHtml(grade) + '">送信する 📨</button>' +
154            '</div>' +
155            '<div class="bb-comment-error" id="bb-comment-error-' + themeId + '"></div></div>';
156        section.insertAdjacentHTML('afterbegin', html);
157        initCommentForm(themeId);
158    }
159
160    /* ── コメントフォーム ── */
161    function initCommentForm(themeId) {
162        var input     = document.getElementById('bb-comment-input-' + themeId);
163        var submitBtn = document.getElementById('bb-comment-submit-' + themeId);
164        if (!input || !submitBtn || submitBtn.dataset.inited) return;
165        submitBtn.dataset.inited = '1';
166
167        var charEl = document.getElementById('bb-char-' + themeId);
168        input.addEventListener('input', function () {
169            if (charEl) charEl.textContent = input.value.length;
170        });
171
172        submitBtn.addEventListener('click', function () {
173            var comment = input.value.trim();
174            var errEl   = document.getElementById('bb-comment-error-' + themeId);
175            if (!comment || comment.length < 5) { showErr(errEl, '5文字以上入力してください。'); return; }
176
177            var wrap   = document.getElementById('bb-battle-' + themeId);
178            var choice = submitBtn.dataset.choice || (wrap && wrap.dataset.myChoice) || '';
179            var grade  = submitBtn.dataset.grade || getSelectedGrade(themeId) || '';
180            if (!choice || !grade) { showErr(errEl, '先に投票して学年を選んでください。'); return; }
181
182            submitBtn.disabled = true;
183            submitBtn.textContent = '送信中…';
184            hideErr(errEl);
185
186            $.ajax({
187                url: BB_Config.ajax_url, method: 'POST',
188                data: { action: 'bb_submit_comment', nonce: BB_Config.nonce, theme_id: themeId, choice: choice, grade: grade, comment: comment },
189                success: function (res) {
190                    if (res.success) {
191                        if ( res.data && res.data.quarantined ) {
192                            var formWrap2 = document.getElementById('bb-comment-form-wrap-' + themeId);
193                            if (formWrap2) formWrap2.outerHTML = '<div class="bb-commented-done">📝 コメントを受け付けました。確認後に公開されます。</div>';
194                        } else {
195                            appendComment(themeId, res.data, wrap);
196                            var formWrap = document.getElementById('bb-comment-form-wrap-' + themeId);
197                            if (formWrap) formWrap.outerHTML = '<div class="bb-commented-done">💬 コメントを投稿しました!ありがとう✨</div>';
198                            // GA イベント
199                            bbTrack('comment', { theme_id: themeId, choice: choice });
200                        }
201                    } else {
202                        showErr(errEl, res.data ? res.data.message : '送信できませんでした。');
203                        submitBtn.disabled = false;
204                        submitBtn.textContent = '送信する 📨';
205                    }
206                },
207                error: function () {
208                    showErr(errEl, '通信エラーが発生しました。');
209                    submitBtn.disabled = false;
210                    submitBtn.textContent = '送信する 📨';
211                }
212            });
213        });
214    }
215
216    /* ── コメントDOM追加 ── */
217    function appendComment(themeId, data, wrap) {
218        var list      = document.getElementById('bb-comment-list-' + themeId);
219        var noComment = document.getElementById('bb-no-comments-' + themeId);
220        var cntEl     = document.getElementById('bb-comment-count-' + themeId);
221        var colorA    = wrap ? wrap.dataset.colorA : '#FF6B6B';
222        var colorB    = wrap ? wrap.dataset.colorB : '#4ECDC4';
223        var optA      = wrap ? wrap.dataset.labelA : 'A';
224        var optB      = wrap ? wrap.dataset.labelB : 'B';
225        var tagColor  = data.choice === 'a' ? colorA : colorB;
226        var tagLabel  = data.choice === 'a' ? optA : optB;
227
228        var reactHtml = '';
229        var reactDefs = {like:'👍',lol:'😂',wow:'😲',fire:'🔥',think:'🤔'};
230        Object.keys(reactDefs).forEach(function (k) {
231            reactHtml += '<button class="bb-react-btn" data-comment-id="' + data.comment_id + '" data-reaction="' + k + '">' + reactDefs[k] + ' <span class="bb-react-count"></span></button>';
232        });
233
234        var html = '<div class="bb-comment-item bb-comment-' + escHtml(data.choice) + ' bb-new-comment" data-comment-id="' + data.comment_id + '">' +
235            '<div class="bb-comment-meta">' +
236            '<span class="bb-comment-grade">' + escHtml(data.grade) + '</span>' +
237            '<span class="bb-comment-choice-tag" style="background:' + escHtml(tagColor) + '">' + escHtml(tagLabel) + '</span>' +
238            '</div>' +
239            '<div class="bb-comment-text">' + escHtml(data.comment) + '</div>' +
240            '<div class="bb-comment-actions">' +
241            '<button class="bb-like-btn" data-comment-id="' + data.comment_id + '">❤️ <span class="bb-like-count">0</span></button>' +
242            '<div class="bb-reactions">' + reactHtml + '</div></div></div>';
243
244        if (noComment) noComment.style.display = 'none';
245        if (cntEl) cntEl.textContent = (parseInt(cntEl.textContent) || 0) + 1;
246
247        if (!list) {
248            var section = document.getElementById('bb-comment-section-' + themeId);
249            if (!section) return;
250            var titleDiv = document.createElement('div');
251            titleDiv.className = 'bb-comment-list-title';
252            titleDiv.innerHTML = '📣 みんなのコメント(<span id="bb-comment-count-' + themeId + '">1</span>件)';
253            section.appendChild(titleDiv);
254            list = document.createElement('div');
255            list.className = 'bb-comment-list';
256            list.id = 'bb-comment-list-' + themeId;
257            section.appendChild(list);
258        }
259        list.insertAdjacentHTML('afterbegin', html);
260        var newItem = list.firstElementChild;
261        if (newItem) {
262            newItem.style.opacity = '0';
263            setTimeout(function () {
264                newItem.style.transition = 'opacity .4s ease';
265                newItem.style.opacity    = '1';
266                initLikeButtons(newItem);
267                initReactionButtons(newItem);
268            }, 50);
269        }
270        // 最大コメントIDを更新
271        var wrap2 = document.getElementById('bb-battle-' + themeId);
272        if (wrap2 && data.comment_id > parseInt(wrap2.dataset.maxCommentId || 0)) {
273            wrap2.dataset.maxCommentId = data.comment_id;
274        }
275    }
276
277    /* ── いいね ── */
278    function initLikeButtons(container) {
279        container.querySelectorAll('.bb-like-btn').forEach(function (btn) {
280            if (btn.dataset.likeInited) return;
281            btn.dataset.likeInited = '1';
282            btn.addEventListener('click', function () {
283                if (btn.classList.contains('liked') || btn.disabled) return;
284                btn.disabled = true;
285                $.ajax({
286                    url: BB_Config.ajax_url, method: 'POST',
287                    data: { action: 'bb_like_comment', nonce: BB_Config.nonce, comment_id: btn.dataset.commentId },
288                    success: function (res) {
289                        if (res.success) {
290                            btn.classList.add('liked');
291                            var c = btn.querySelector('.bb-like-count');
292                            if (c) c.textContent = res.data.likes;
293                            btn.style.transform = 'scale(1.3)';
294                            setTimeout(function () { btn.style.transform = ''; }, 200);
295                            bbTrack('like', { comment_id: btn.dataset.commentId });
296                        } else { btn.disabled = false; }
297                    },
298                    error: function () { btn.disabled = false; }
299                });
300            });
301        });
302    }
303
304    /* ── スタンプリアクション ── */
305    function initReactionButtons(container) {
306        container.querySelectorAll('.bb-react-btn').forEach(function (btn) {
307            if (btn.dataset.reactInited) return;
308            btn.dataset.reactInited = '1';
309            btn.addEventListener('click', function () {
310                $.ajax({
311                    url: BB_Config.ajax_url, method: 'POST',
312                    data: { action: 'bb_add_reaction', nonce: BB_Config.nonce, comment_id: btn.dataset.commentId, reaction: btn.dataset.reaction },
313                    success: function (res) {
314                        if (!res.success) return;
315                        var item = btn.closest('.bb-comment-item');
316                        if (!item) return;
317                        // 全リアクションボタンを更新
318                        item.querySelectorAll('.bb-react-btn').forEach(function (b) {
319                            b.classList.remove('active');
320                            var cnt = res.data.counts[b.dataset.reaction] || 0;
321                            var cEl = b.querySelector('.bb-react-count');
322                            if (cEl) cEl.textContent = cnt > 0 ? cnt : '';
323                        });
324                        if (res.data.my_reaction) {
325                            var activeBtn = item.querySelector('[data-reaction="' + res.data.my_reaction + '"]');
326                            if (activeBtn) activeBtn.classList.add('active');
327                        }
328                    }
329                });
330            });
331        });
332    }
333
334    /* ── 新着コメントポーリング ── */
335    function startPolling(themeId) {
336        setInterval(function () {
337            var wrap = document.getElementById('bb-battle-' + themeId);
338            if (!wrap) return;
339            var afterId = parseInt(wrap.dataset.maxCommentId || 0);
340            $.ajax({
341                url: BB_Config.ajax_url, method: 'POST',
342                data: { action: 'bb_get_new_comments', nonce: BB_Config.nonce, theme_id: themeId, after_id: afterId },
343                success: function (res) {
344                    if (!res.success || !res.data.comments.length) return;
345                    // 投票数も更新
346                    $.ajax({
347                        url: BB_Config.ajax_url, method: 'POST',
348                        data: { action: 'bb_get_results', nonce: BB_Config.nonce, theme_id: themeId },
349                        success: function (r2) { if (r2.success) updateResultDisplay(themeId, r2.data.votes); }
350                    });
351                    res.data.comments.forEach(function (c) {
352                        // 既に表示済みのコメントはスキップ
353                        if (document.querySelector('[data-comment-id="' + c.id + '"]')) return;
354                        appendCommentFromPoll(themeId, c, wrap);
355                    });
356                }
357            });
358        }, POLL_INTERVAL);
359    }
360
361    function appendCommentFromPoll(themeId, c, wrap) {
362        var colorA   = wrap ? wrap.dataset.colorA : '#FF6B6B';
363        var colorB   = wrap ? wrap.dataset.colorB : '#4ECDC4';
364        var optA     = wrap ? wrap.dataset.labelA : 'A';
365        var optB     = wrap ? wrap.dataset.labelB : 'B';
366        var tagColor = c.choice === 'a' ? colorA : colorB;
367        var tagLabel = c.choice === 'a' ? optA : optB;
368
369        var reactDefs = {like:'👍',lol:'😂',wow:'😲',fire:'🔥',think:'🤔'};
370        var reactHtml = '';
371        Object.keys(reactDefs).forEach(function (k) {
372            var cnt = (c.reactions && c.reactions[k]) ? c.reactions[k] : 0;
373            var active = c.my_reaction === k ? ' active' : '';
374            reactHtml += '<button class="bb-react-btn' + active + '" data-comment-id="' + c.id + '" data-reaction="' + k + '">' + reactDefs[k] + ' <span class="bb-react-count">' + (cnt||'') + '</span></button>';
375        });
376
377        var likedCls = c.i_liked ? ' liked' : '';
378        var html = '<div class="bb-comment-item bb-comment-' + escHtml(c.choice) + ' bb-new-comment" data-comment-id="' + c.id + '">' +
379            '<div class="bb-comment-meta"><span class="bb-comment-grade">' + escHtml(c.grade) + '</span>' +
380            '<span class="bb-comment-choice-tag" style="background:' + escHtml(tagColor) + '">' + escHtml(tagLabel) + '</span></div>' +
381            '<div class="bb-comment-text">' + escHtml(c.comment) + '</div>' +
382            (c.admin_reply ? '<div class="bb-admin-reply"><span class="bb-admin-reply-label">📝 運営より</span>' + escHtml(c.admin_reply) + '</div>' : '') +
383            '<div class="bb-comment-actions"><button class="bb-like-btn' + likedCls + '" data-comment-id="' + c.id + '">❤️ <span class="bb-like-count">' + c.likes + '</span></button>' +
384            '<div class="bb-reactions">' + reactHtml + '</div></div></div>';
385
386        var list = document.getElementById('bb-comment-list-' + themeId);
387        if (!list) return;
388        list.insertAdjacentHTML('afterbegin', html);
389        var newItem = list.firstElementChild;
390        if (newItem) {
391            newItem.style.opacity = '0';
392            setTimeout(function () { newItem.style.transition = 'opacity .4s'; newItem.style.opacity = '1'; initLikeButtons(newItem); initReactionButtons(newItem); }, 50);
393        }
394        var cntEl = document.getElementById('bb-comment-count-' + themeId);
395        if (cntEl) cntEl.textContent = (parseInt(cntEl.textContent) || 0) + 1;
396        if (c.id > parseInt(wrap.dataset.maxCommentId || 0)) wrap.dataset.maxCommentId = c.id;
397    }
398
399    /* ── マイルストーン演出 ── */
400    function showMilestone(ms) {
401        var overlay = document.getElementById('bb-milestone-overlay');
402        if (!overlay) return;
403        var msgEl   = document.getElementById('bb-milestone-msg');
404        if (msgEl) msgEl.textContent = ms.msg;
405        overlay.style.display = 'flex';
406        setTimeout(function () { overlay.style.display = 'none'; }, 3500);
407        overlay.addEventListener('click', function () { overlay.style.display = 'none'; });
408    }
409
410    /* ── カウントダウン ── */
411    function initCountdown(wrap) {
412        var el = wrap.querySelector('.bb-countdown[data-end-ts]');
413        if (!el) return;
414        var endTs = parseInt(el.dataset.endTs) * 1000;
415        function update() {
416            var diff = endTs - Date.now();
417            if (diff <= 0) { el.textContent = '終了'; return; }
418            var days  = Math.floor(diff / 86400000);
419            var hours = Math.floor((diff % 86400000) / 3600000);
420            var mins  = Math.floor((diff % 3600000) / 60000);
421            if (days > 0)       el.innerHTML = '残り <span class="bb-count-days">' + days + '</span>日';
422            else if (hours >
422 0) el.innerHTML = '残り <span class="bb-count-days">' + hours + '</span>時間 ' + mins + '分';
423            else                el.innerHTML = '残り <span class="bb-count-days">' + mins + '</span>分';
424            setTimeout(update, 30000);
425        }
426        update();
427    }
428
429    /* ── ユーティリティ ── */
430    function getSelectedGrade(themeId) {
431        var cont = document.getElementById('bb-grade-' + themeId);
432        if (!cont) return '';
433        var sel = cont.querySelector('.bb-grade-btn.selected');
434        return sel ? sel.dataset.grade : '';
435    }
436    function setText(id, val) { var el = document.getElementById(id); if (el) el.textContent = val; }
437    function showErr(el, msg) { if (!el) return; el.textContent = msg; el.style.display = 'block'; }
438    function hideErr(el)      { if (!el) return; el.textContent = ''; el.style.display = 'none'; }
439    function fmt(n) { return (n||0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
440    function escHtml(s) {
441        return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
442    }
443
444    /**
445     * GA4 イベント送信(gtag.js が読み込まれている場合のみ動作)
446     * イベント名: bb_vote / bb_comment / bb_like / bb_reaction
447     *
448     * 使い方:
449     *   WordPressの管理画面 > 設定 > Google Analyticsなどで
450     *   Measurement ID(G-XXXXXXXXXX)を設定しているだけで自動送信されます。
451     *   カスタムレポートは GA4 > イベント > "bb_vote" などで確認できます。
452     */
453    function bbTrack(eventName, params) {
454        try {
455            if (typeof window.gtag === 'function') {
456                window.gtag('event', 'bb_' + eventName, params);
457            }
458            // UA(旧GA)にも対応
459            if (typeof window.ga === 'function') {
460                window.ga('send', 'event', 'BunkenBattle', eventName, JSON.stringify(params));
461            }
462        } catch(e) {}
463    }
464
465})(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.