/* 仅解除正文的选择 / 右键内联限制;保留并恢复网页自身的设置。 */ (function (global) { 'use strict'; const EVENT_FIELDS = ['onselectstart', 'oncontextmenu']; function inheritedDescriptor(node, name) { let prototype = Object.getPrototypeOf(node); while (prototype) { const descriptor = Object.getOwnPropertyDescriptor(prototype, name); if (descriptor) return descriptor; prototype = Object.getPrototypeOf(prototype); } return null; } function create() { const doc = global.document; const records = new Map(); let enabled = false; let destroyed = false; let style = null; let observer = null; function readNative(record, field) { try { if (field.native && typeof field.native.get === 'function') return field.native.get.call(record.node); return null; } catch (_) { return null; } } function writeNative(record, field, value) { try { if (field.native && typeof field.native.set === 'function') field.native.set.call(record.node, value); } catch (_) { /* 不覆盖不允许修改的网页字段。 */ } } function removeRestriction(record, field) { if (record.node.hasAttribute(field.name)) record.removeAttribute.call(record.node, field.name); writeNative(record, field, null); } function rememberAttribute(record, field) { field.hasAttribute = record.node.hasAttribute(field.name); field.attributeValue = record.node.getAttribute(field.name); field.intendedProperty = readNative(record, field); field.propertyAssigned = false; removeRestriction(record, field); } function installField(record, name) { const node = record.node; const own = Object.getOwnPropertyDescriptor(node, name); const native = inheritedDescriptor(node, name); const field = { name: name, own: own, native: native, hasAttribute: node.hasAttribute(name), attributeValue: node.getAttribute(name), intendedProperty: node[name], propertyAssigned: false, getter: null, setter: null }; record.fields[name] = field; removeRestriction(record, field); // 实例级别拦截这两个属性的重新赋值,不改变事件的点击行为。 if ((!own || own.configurable) && native && typeof native.set === 'function') { field.getter = function () { return null; }; field.setter = function (value) { writeNative(record, field, value); field.intendedProperty = readNative(record, field); field.propertyAssigned = true; writeNative(record, field, null); }; try { Object.defineProperty(node, name, { configurable: true, enumerable: own ? own.enumerable : true, get: field.getter, set: field.setter }); } catch (_) { field.getter = field.setter = null; } } } function installMethod(record, name) { const node = record.node; const own = Object.getOwnPropertyDescriptor(node, name); const original = node[name]; const wrapper = function (attributeName) { const result = original.apply(this, arguments); if (enabled && this === node) { const field = record.fields[String(attributeName).toLowerCase()]; if (field) rememberAttribute(record, field); } return result; }; if (!own || own.configurable) { try { Object.defineProperty(node, name, { configurable: true, writable: true, enumerable: own ? own.enumerable : false, value: wrapper }); record.methods[name] = { own: own, wrapper: wrapper }; } catch (_) { /* MutationObserver 仍会处理内联属性的重新设置。 */ } } } function watchBody() { if (!enabled || !doc.body || records.has(doc.body)) return; const node = doc.body; const record = { node: node, fields: {}, methods: {}, setAttribute: node.setAttribute, removeAttribute: node.removeAttribute }; records.set(node, record); EVENT_FIELDS.forEach(function (name) { installField(record, name); }); // 记录正文上对限制属性的最新修改意图。其他属性原样转交浏览器。 installMethod(record, 'setAttribute'); installMethod(record, 'removeAttribute'); } function restoreRecord(record) { const node = record.node; Object.keys(record.methods).forEach(function (name) { const method = record.methods[name]; if (node[name] !== method.wrapper) return; try { if (method.own) Object.defineProperty(node, name, method.own); else delete node[name]; } catch (_) { /* 若网页已接管字段,保留网页的新设置。 */ } }); EVENT_FIELDS.forEach(function (name) { const field = record.fields[name]; const current = Object.getOwnPropertyDescriptor(node, name); const ownsProperty = !field.getter || current && current.get === field.getter && current.set === field.setter; if (field.getter && !ownsProperty) return; if (field.getter && ownsProperty) { try { if (field.own) Object.defineProperty(node, name, field.own); else delete node[name]; } catch (_) { /* 保留不能恢复的字段。 */ } } // 当前仍有内联值时,说明网页刚改过它且观察回调尚未执行,保留该值。 if (node.hasAttribute(name)) return; if (field.hasAttribute) record.setAttribute.call(node, name, field.attributeValue); else record.removeAttribute.call(node, name); if (ownsProperty) { // 属性和事件函数可被分别设置,恢复实际保存的最后一个处理器。 writeNative(record, field, field.intendedProperty); // 先前实例上的自定义值不属于原生事件处理器,恢复其最新值。 if (field.own && 'value' in field.own && field.propertyAssigned) { try { node[name] = field.intendedProperty; } catch (_) { /* 只读属性保持原样。 */ } } } }); } function start() { style = doc.createElement('style'); style.setAttribute('data-yian-copy-style', ''); style.textContent = 'body,body *:not(input):not(button):not(select):not(option):not(textarea):not([data-yian-assistant]){-webkit-user-select:text!important;user-select:text!important;}'; (doc.head || doc.documentElement).appendChild(style); watchBody(); observer = new global.MutationObserver(function (mutations) { if (!enabled) return; watchBody(); mutations.forEach(function (mutation) { if (mutation.type !== 'attributes') return; const record = records.get(mutation.target); if (!record) return; const field = record.fields[mutation.attributeName]; // 自己移除的属性已是 null;有新的内联限制才记录并解除。 if (field && record.node.hasAttribute(field.name)) rememberAttribute(record, field); }); }); observer.observe(doc.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: EVENT_FIELDS }); } function stop() { if (observer) { observer.disconnect(); observer = null; } records.forEach(restoreRecord); records.clear(); if (style) { style.remove(); style = null; } } function setEnabled(value) { if (destroyed || enabled === Boolean(value)) return; enabled = Boolean(value); if (enabled) start(); else stop(); } function destroy() { if (destroyed) return; enabled = false; stop(); destroyed = true; } return { setEnabled: setEnabled, destroy: destroy }; } global.YianCopyController = { create: create }; })(typeof window !== 'undefined' ? window : globalThis);