177 lines
7.7 KiB
JavaScript
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);
|