PageSourceSearch

http://vortaro.cn/js/copy-guard.js?v=20260509

js vortaro.cn collected 2026-10-06 13:15:12 UTC 6,485 bytes, 195 lines download raw bytes

1/**
2 * 复制保护模块
3 * 文件: js/copy-guard.js
4 * 功能:限制用户每次只能复制一个段落的内容,防止大范围复制
5 */
6
7(function() {
8    'use strict';
9    
10    if (window._copyGuardLoaded) return;
11    window._copyGuardLoaded = true;
12    
13    console.log('复制保护模块已加载');
14    
15    /**
16     * 标准化文本(去除多余空白)
17     */
18    function normalizeText(txt) {
19        if (!txt) return '';
20        return txt.replace(/\s+/g, ' ').trim();
21    }
22    
23    /**
24     * 判断段落文本是否包含选中文本
25     */
26    function isTextContained(paraText, selectedText) {
27        var pNorm = normalizeText(paraText);
28        var sNorm = normalizeText(selectedText);
29        if (sNorm.length === 0) return true;
30        if (pNorm.length === 0) return false;
31        // 检查选中文本是否在段落文本中
32        return pNorm.indexOf(sNorm) !== -1;
33    }
34    
35    /**
36     * 显示复制提示
37     */
38    function showCopyTip(message) {
39        var oldTip = document.querySelector('.copy-tip');
40        if (oldTip) oldTip.remove();
41        
42        var tip = document.createElement('div');
43        tip.className = 'copy-tip';
44        tip.textContent = message;
45        document.body.appendChild(tip);
46        setTimeout(function() { if (tip.parentNode) tip.remove(); }, 1500);
47    }
48    
49    /**
50     * 复制事件处理器
51     */
52    function paragraphCopyHandler(e) {
53        var sel = window.getSelection();
54        var selectedText = sel.toString();
55        
56        if (!selectedText || selectedText.length === 0) {
57            return;
58        }
59        
60        // 获取选中内容的起始和结束容器
61        if (sel.rangeCount === 0) return;
62        var range = sel.getRangeAt(0);
63        
64        // 查找起始和结束段落
65        function findParaNode(node) {
66            var el = node.nodeType === 3 ? node.parentNode : node;
67            while (el && el !== document.body && el !== document.getElementById('webBrowser')) {
68                if (el.hasAttribute && el.hasAttribute('data-para-id')) {
69                    return el;
70                }
71                el = el.parentNode;
72            }
73            return null;
74        }
75        
76        var startPara = findParaNode(range.startContainer);
77        var endPara = findParaNode(range.endContainer);
78        
79        // 如果跨段落(起始和结束段落不同),限制复制
80        if (startPara && endPara && startPara !== endPara) {
81            e.preventDefault();
82            e.stopPropagation();
83            
84            // 显示提示
85            showCopyTip('只能复制单个段落的内容');
86            
87            // 自动选择起始段落
88            sel.removeAllRanges();
89            var newRange = document.createRange();
90            newRange.selectNodeContents(startPara);
91            sel.addRange(newRange);
92            
93            // 延迟执行复制
94            setTimeout(function() {
95                try {
96                    document.execCommand('copy');
97                    console.log('已自动复制当前段落');
98                } catch(err) {
99                    console.warn('复制失败:', err);
100                }
101            }, 50);
102            return;
103        }
104        
105        // 如果没有找到段落标识,或者在同一段落内,允许正常复制
106        console.log('正常复制,选中文本长度:', selectedText.length);
107    }
108    
109    /**
110     * 为所有受保护段落附加监听器
111     */
112    function attachCopyListeners() {
113        var paras = document.querySelectorAll('[data-para-id]');
114        for (var i = 0; i < paras.length; i++) {
115            var p = paras[i];
116            p.removeEventListener('copy', paragraphCopyHandler);
117            p.removeEventListener('cut', paragraphCopyHandler);
118            p.addEventListener('copy', paragraphCopyHandler);
119            p.addEventListener('cut', paragraphCopyHandler);
120        }
121        
122        if (paras.length > 0) {
123            console.log('已为 ' + paras.length + ' 个段落附加复制保护监听器');
124        }
125    }
126    
127    /**
128     * 禁用全选功能(Ctrl+A)
129     */
130    function disableSelectAll() {
131        document.addEventListener('keydown', function(e) {
132            if (e.ctrlKey && e.key === 'a') {
133                e.preventDefault();
134                showCopyTip('请手动选择要复制的段落');
135                
136                // 可选:自动选择第一个段落
137                var firstPara = document.querySelector('[data-para-id]');
138                if (firstPara) {
139                    var sel = window.getSelection();
140                    sel.removeAllRanges();
141                    var range = document.createRange();
142                    range.selectNodeContents(firstPara);
143                    sel.addRange(range);
144                }
145            }
146        });
147    }
148    
149    /**
150     * 初始化复制保护
151     */
152    function initCopyGuard() {
153        // 延迟执行,等待DOM加载
154        setTimeout(function() {
155            attachCopyListeners();
156            disableSelectAll();
157        }, 200);
158        
159        // 使用 MutationObserver 监听动态添加的内容
160        var observer = new MutationObserver(function(mutations) {
161            var needAttach = false;
162            for (var i = 0; i < mutations.length; i++) {
163                if (mutations[i].addedNodes.length > 0) {
164                    needAttach = true;
165                    break;
166                }
167            }
168            if (needAttach) {
169                setTimeout(attachCopyListeners, 200);
170            }
171        });
172        
173        var targetNode = document.getElementById('webBrowser');
174        if (targetNode) {
175            observer.observe(targetNode, { childList: true, subtree: true });
176        }
177        
178        // 定期重新附加(保险措施)
179        setInterval(attachCopyListeners, 3000);
180    }
181    
182    // 页面加载完成后初始化
183    if (document.readyState === 'loading') {
184        document.addEventListener('DOMContentLoaded', initCopyGuard);
185    } else {
186        initCopyGuard();
187    }
188    
189    // 导出全局方法
190    window.attachCopyListeners = attachCopyListeners;
191    window.reinitCopyGuard = function() {
192        attachCopyListeners();
193    };
194    
195})();

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.