Files
yianxue-assistant/copy-controller.js
2026-10-10 03:39:43 +08:00

177 lines
7.7 KiB
JavaScript

/* 仅解除正文的选择 / 右键内联限制;保留并恢复网页自身的设置。 */
(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);