PageSourceSearch

https://cdn.unwire.hk/wp-content/themes/unwire/static/js/unwire-like-ui.js?ver=1790167399

js unwire.hk collected 2026-09-24 17:38:01 UTC 14,830 bytes, 369 lines download raw bytes

1(function(window, document, $) {
2    'use strict';
3
4    if (!$) return;
5
6    var state = {
7        initialized: false,
8        beaconTimer: null,
9        beaconNode: null
10    };
11
12    function isIOSDevice() {
13        var userAgent = window.navigator && window.navigator.userAgent ? window.navigator.userAgent : '';
14        var platform = window.navigator && window.navigator.platform ? window.navigator.platform : '';
15        var maxTouchPoints = window.navigator && typeof window.navigator.maxTouchPoints === 'number' ? window.navigator.maxTouchPoints : 0;
16
17        return /iPad|iPhone|iPod/.test(userAgent) || (platform === 'MacIntel' && maxTouchPoints > 1);
18    }
19
20    function formatLikeCount(count) {
21        var value = parseInt(count, 10) || 0;
22        return value.toLocaleString('en-US');
23    }
24
25    function getLikeWidgetState($widget) {
26        return {
27            liked: $widget.hasClass('is-liked'),
28            count: parseInt($widget.find('.unwire-like-count').first().text().replace(/,/g, ''), 10) || 0
29        };
30    }
31
32    function resolveRootScope($scope) {
33        return ($scope && $scope.length) ? $scope : $(document);
34    }
35
36    function resolveRefreshScope($widget) {
37        var $article = $widget.closest('.main-article');
38        return $article.length ? $article : $(document);
39    }
40
41    function getCommentBeaconLines() {
42        var fallback = [
43            'iOS 用家注意 :',
44            '按住 💬 一秒',
45            '選「在新分頁開啟」'
46        ];
47
48        if (!window.unwire_like_settings || !$.isArray(window.unwire_like_settings.comment_beacon_lines)) {
49            return fallback;
50        }
51
52        var lines = $.map(window.unwire_like_settings.comment_beacon_lines, function(line) {
53            line = typeof line === 'string' ? $.trim(line) : '';
54            return line ? line : null;
55        });
56
57        return lines.length ? lines : fallback;
58    }
59
60    function getCommentBeacon() {
61        if (state.beaconNode) {
62            return state.beaconNode;
63        }
64
65        var beacon = document.createElement('div');
66        var lines = getCommentBeaconLines();
67        beacon.className = 'unwire-comment-beacon';
68        beacon.setAttribute('aria-live', 'polite');
69        beacon.setAttribute('aria-atomic', 'true');
70        beacon.style.position = 'fixed';
71        beacon.style.zIndex = '9999';
72        beacon.style.maxWidth = 'min(19rem, calc(100vw - 1.5rem))';
73        beacon.style.padding = '0.95rem 1rem 0.9rem';
74        beacon.style.borderRadius = '16px';
75        beacon.style.background = 'rgba(16, 18, 20, 0.94)';
76        beacon.style.color = '#ffffff';
77        beacon.style.fontSize = '16px';
78        beacon.style.fontWeight = '700';
79        beacon.style.lineHeight = '1.4';
80        beacon.style.letterSpacing = '0.01em';
81        beacon.style.boxShadow = '0 18px 36px rgba(0, 0, 0, 0.28)';
82        beacon.style.border = '1px solid rgba(255,255,255,0.12)';
83        beacon.style.opacity = '0';
84        beacon.style.pointerEvents = 'none';
85        beacon.style.transform = 'translate3d(-50%, -92%, 0) scale(0.96)';
86        beacon.style.transition = 'opacity 180ms ease, transform 180ms ease';
87        beacon.style.textAlign = 'left';
88        beacon.style.display = 'none';
89        beacon.innerHTML = $.map(lines, function(line) {
90            return '<span style="display:block;">' + $('<div>').text(line).html() + '</span>';
91        }).join('');
92        document.body.appendChild(beacon);
93        state.beaconNode = beacon;
94
95        return beacon;
96    }
97
98    function showCommentBeacon(target, event) {
99        if (!target || !target.getBoundingClientRect) return;
100
101        var beacon = getCommentBeacon();
102        var rect = target.getBoundingClientRect();
103        var viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0;
104        var originalEvent = event && event.originalEvent ? event.originalEvent : event;
105        var pointX = 0;
106        var pointY = 0;
107
108        beacon.style.display = 'block';
109        if (originalEvent && originalEvent.changedTouches && originalEvent.changedTouches.length) {
110            pointX = originalEvent.changedTouches[0].clientX;
111            pointY = originalEvent.changedTouches[0].clientY;
112        } else if (originalEvent && typeof originalEvent.clientX === 'number' && typeof originalEvent.clientY === 'number') {
113            pointX = originalEvent.clientX;
114            pointY = originalEvent.clientY;
115        } else {
116            pointX = rect.left + (rect.width / 2);
117            pointY = rect.top + Math.min(rect.height, 26);
118        }
119
120        var beaconRect = beacon.getBoundingClientRect();
121        var left = pointX + 18;
122        var top = pointY - 92;
123
124        left = Math.max((beaconRect.width / 2) + 12, Math.min(viewportWidth - (beaconRect.width / 2) - 12, left));
125        top = Math.max(20, top);
126
127        beacon.style.left = Math.round(left) + 'px';
128        beacon.style.top = Math.round(top) + 'px';
129
130        window.clearTimeout(state.beaconTimer);
131        window.requestAnimationFrame(function() {
132            beacon.style.opacity = '1';
133            beacon.style.transform = 'translate3d(-50%, -100%, 0) scale(1)';
134        });
135
136        state.beaconTimer = window.setTimeout(function() {
137            beacon.style.opacity = '0';
138            beacon.style.transform = 'translate3d(-50%, -92%, 0) scale(0.96)';
139            window.setTimeout(function() {
140                beacon.style.display = 'none';
141            }, 180);
142        }, 3000);
143    }
144
145    function applyLikeSummaryToPost(postId, summary, $scope) {
146        if (!postId || !summary) return;
147
148        var widgetSelector = '.unwire-like-widget[data-post-id="' + postId + '"]';
149        var $widgets = ($scope && $scope.length) ? $scope.find(widgetSelector) : $(widgetSelector);
150        if (!$widgets.length && $scope && $scope.length) {
151            $widgets = $(widgetSelector);
152        }
153
154        $widgets.each(function() {
155            var $widget = $(this);
156            var liked = !!summary.liked;
157            var count = parseInt(summary.count, 10) || 0;
158            var $button = $widget.find('.unwire-like-button').first();
159            var $label = $widget.find('.unwire-like-button-label').first();
160            var $counts = $widget.find('.unwire-like-count');
161            var $countLabel = $widget.find('.unwire-like-count-label').first();
162
163            $widget.attr('data-loaded', '1');
164            $widget.toggleClass('is-liked', liked).removeClass('is-busy');
165            if ($widget.hasClass('unwire-like-widget-preview-card')) {
166                $widget.toggleClass('is-zero-count', count === 0);
167            }
168            $button.attr('aria-pressed', liked ? 'true' : 'false');
169            if ($label.length) {
170                $label.text(liked ? unwire_like_settings.labels.liked : unwire_like_settings.labels.like);
171            }
172            if ($counts.length) {
173                $counts.text(formatLikeCount(count));
174            }
175            if ($countLabel.length) {
176                $countLabel.text(unwire_like_settings.labels.count_suffix);
177            }
178        });
179    }
180
181    function applyCommentSummaryToPost(postId, summary, $scope) {
182        if (!postId || !summary) return;
183
184        var commentCount = parseInt(summary.comment_count, 10) || 0;
185        var commentUrl = summary.comment_url || '';
186        var commentRel = summary.comment_rel || 'nofollow noopener noreferrer external';
187        var hasCommentUrl = !!summary.has_comment_url;
188        var isCommentEnabled = hasCommentUrl && commentCount > 0;
189        var linkSelector = '.single-post-preview-chat-dummy[data-post-id="' + postId + '"], .post-footer-comments-button[data-post-id="' + postId + '"], .homepage-latest-comment-link[data-post-id="' + postId + '"]';
190        var $links = ($scope && $scope.length) ? $scope.find(linkSelector) : $(linkSelector);
191        if (!$links.length && $scope && $scope.length) {
192            $links = $(linkSelector);
193        }
194
195        $links.each(function() {
196            var $link = $(this);
197            var $count = $link.find('.single-post-preview-chat-dummy-count, .post-footer-comments-button-count, .homepage-latest-comment-count').first();
198
199            $link.attr('data-comment-url', commentUrl);
200            $link.attr('data-has-comment-url', hasCommentUrl ? '1' : '0');
201
202            if ($count.length) {
203                $count.text(formatLikeCount(commentCount));
204            }
205
206            if (isCommentEnabled) {
207                $link.attr({
208                    href: commentUrl,
209                    target: '_blank',
210                    rel: commentRel
211                }).removeAttr('aria-disabled tabindex').css('pointer-events', '');
212            } else {
213                $link.attr({
214                    href: '#',
215                    'aria-disabled': 'true',
216                    tabindex: '-1'
217                }).removeAttr('target rel').css('pointer-events', '');
218            }
219
220            $link.prop('hidden', false).css('display', '');
221            $link.toggleClass('is-zero-count', commentCount <= 0);
222            $link.toggleClass('is-disabled', !isCommentEnabled);
223        });
224    }
225
226    function applyInitialCommentVisibility($scope) {
227        var $root = resolveRootScope($scope);
228        var linkSelector = '.single-post-preview-chat-dummy[data-post-id], .post-footer-comments-button[data-post-id], .homepage-latest-comment-link[data-post-id]';
229        var $links = $root.find(linkSelector);
230
231        if (!$links.length && $root[0] !== document) {
232            $links = $(linkSelector);
233        }
234
235        $links.each(function() {
236            var $link = $(this);
237            var postId = parseInt($link.data('postId'), 10) || 0;
238            var commentCount = parseInt($link.find('.single-post-preview-chat-dummy-count, .post-footer-comments-button-count, .homepage-latest-comment-count').first().text().replace(/,/g, ''), 10) || 0;
239            var commentUrl = $link.attr('data-comment-url') || '';
240            var hasCommentUrl = $link.attr('data-has-comment-url') === '1';
241
242            applyCommentSummaryToPost(postId, {
243                comment_count: commentCount,
244                comment_url: commentUrl,
245                comment_rel: $link.attr('rel') || 'nofollow noopener noreferrer external',
246                has_comment_url: hasCommentUrl
247            }, $root);
248        });
249    }
250
251    function refresh($scope) {
252        var $root = resolveRootScope($scope);
253        var ids = [];
254
255        $root.find('.unwire-like-widget').each(function() {
256            var $widget = $(this);
257            var postId = parseInt($widget.data('postId'), 10) || 0;
258            if (!postId || $widget.attr('data-loaded') === '1' || $widget.hasClass('is-busy')) return;
259            ids.push(postId);
260            $widget.addClass('is-busy');
261        });
262
263        ids = $.grep(ids, function(value, index) {
264            return $.inArray(value, ids) === index;
265        });
266
267        if (!ids.length) return;
268
269        $.ajax({
270            method: 'POST',
271            dataType: 'json',
272            url: ajax_object.ajax_url,
273            data: {
274                action: 'unwire_get_like_summary',
275                post_ids: ids,
276                nonce: unwire_like_settings.nonce
277            }
278        }).done(function(response) {
279            if (!response || !response.success || !response.data || !response.data.summaries) return;
280
281            $.each(response.data.summaries, function(postId, summary) {
282                applyLikeSummaryToPost(postId, summary, $root);
283                applyCommentSummaryToPost(postId, summary, $root);
284            });
285        }).always(function() {
286            var busySelector = '.unwire-like-widget';
287            var $busyWidgets = $root.find(busySelector);
288            if (!$busyWidgets.length && $root[0] !== document) {
289                $busyWidgets = $('.unwire-like-widget');
290            }
291
292            $.each(ids, function(_, postId) {
293                $busyWidgets.filter('[data-post-id="' + postId + '"]').removeClass('is-busy');
294            });
295        });
296    }
297
298    function bindEvents() {
299        $('body').on('click.unwire-like-disabled', '.single-post-preview-chat-dummy[aria-disabled="true"], .post-footer-comments-button[aria-disabled="true"], .homepage-latest-comment-link[aria-disabled="true"]', function(event) {
300            event.preventDefault();
301        });
302
303        $('body').on('click.unwire-comment-beacon', '.single-post-preview-chat-dummy, .post-footer-comments-button, .homepage-latest-comment-link', function(event) {
304            var href = this.getAttribute('href') || '';
305
306            if (!isIOSDevice()) return;
307            if (this.getAttribute('aria-disabled') === 'true') return;
308            if (!href || href === '#') return;
309
310            event.preventDefault();
311            showCommentBeacon(this, event);
312        });
313
314        $('body').on('click.unwire-like-toggle', '.unwire-like-button', function() {
315            var $button = $(this);
316            var $widget = $button.closest('.unwire-like-widget');
317            var postId = parseInt($widget.data('postId'), 10) || 0;
318            var currentState = getLikeWidgetState($widget);
319            var nextLiked = !currentState.liked;
320            var nextCount = currentState.count + (nextLiked ? 1 : -1);
321            var $refreshScope = resolveRefreshScope($widget);
322
323            if (!postId || $widget.hasClass('is-busy')) return;
324
325            nextCount = Math.max(0, nextCount);
326            $widget.addClass('is-busy');
327            applyLikeSummaryToPost(postId, {
328                liked: nextLiked,
329                count: nextCount
330            });
331
332            $.ajax({
333                method: 'POST',
334                dataType: 'json',
335                url: ajax_object.ajax_url,
336                data: {
337                    action: 'unwire_toggle_like',
338                    post_id: postId,
339                    liked: nextLiked ? 1 : 0,
340                    nonce: unwire_like_settings.nonce
341                }
342            }).done(function(response) {
343                if (!response || !response.success || !response.data) {
344                    applyLikeSummaryToPost(postId, currentState);
345                    refresh($refreshScope);
346                    return;
347                }
348                applyLikeSummaryToPost(postId, response.data);
349            }).fail(function() {
350                applyLikeSummaryToPost(postId, currentState);
351                refresh($refreshScope);
352            }).always(function() {
353                $widget.removeClass('is-busy');
354            });
355        });
356    }
357
358    function init() {
359        if (state.initialized) return;
360        bindEvents();
361        applyInitialCommentVisibility($(document));
362        state.initialized = true;
363    }
364
365    window.UnwireLikeUI = {
366        init: init,
367        refresh: refresh
368    };
369})(window, document, window.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.