feat: add portable role-based question bank assistant
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
/* 仅解除正文的选择 / 右键内联限制;保留并恢复网页自身的设置。 */
|
||||
(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);
|
||||
Reference in New Issue
Block a user