@yiero/gmlib
Version:
GM Lib for Tampermonkey/ScriptCat
1,372 lines (1,371 loc) • 48.5 kB
JavaScript
const environmentTest = ()=>GM_info.scriptHandler;
function getCookie(content, key) {
const isTextCookie = [
/^\w+=[^=;]+$/,
/^\w+=[^=;]+;/
].some((reg)=>reg.test(content));
if (isTextCookie) {
if (!key) return Promise.reject(new Error(`请输入需要获取的具体 Cookie 的键名.`));
const cookieList = content.split(/;\s?/).map((cookie)=>cookie.split('='));
const cookieMap = new Map(cookieList);
const cookieValue = cookieMap.get(key);
if (!cookieValue) return Promise.reject(new Error('获取 Cookie 失败, key 不存在.'));
return Promise.resolve(cookieValue);
}
return new Promise((resolve, reject)=>{
const env = environmentTest();
if ('ScriptCat' !== env) return reject(new Error(`当前脚本不支持 ${env} 环境, 仅支持 ScriptCat .`));
GM_cookie('list', {
domain: content
}, (cookieList)=>{
if (!cookieList) return void reject(new Error('获取 Cookie 失败, 该域名下没有 cookie. '));
if (!key) resolve(cookieList);
const userIdCookie = cookieList.find((cookie)=>cookie.name === key);
if (!userIdCookie) return void reject(new Error('获取 Cookie 失败, key 不存在. '));
resolve(userIdCookie.value);
});
});
}
const gmDownload = (url, filename, details = {})=>new Promise((resolve, reject)=>{
const abortHandle = GM_download({
url: url,
name: filename,
...details,
onload (event) {
details.onload?.(event);
resolve(true);
},
onerror (err) {
details.onerror?.(err);
reject(err.error);
},
ontimeout () {
details.ontimeout?.();
reject('time_out');
},
onprogress (response) {
details.onprogress?.(response, abortHandle);
}
});
});
gmDownload.blob = async (blob, filename, details = {})=>{
const url = URL.createObjectURL(blob);
return gmDownload(url, filename, details).then((res)=>{
URL.revokeObjectURL(url);
return res;
});
};
gmDownload.text = (content, filename, mimeType = 'text/plain', details = {})=>{
const blob = new Blob([
content
], {
type: mimeType
});
return gmDownload.blob(blob, filename, details);
};
const parseResponseText = (responseText)=>{
try {
return JSON.parse(responseText);
} catch {
try {
const domParser = new DOMParser();
return domParser.parseFromString(responseText, 'text/html');
} catch {
return responseText;
}
}
};
function gmRequest(param1, method, body, GMXmlHttpRequestConfig) {
const unifiedParameters = ()=>{
if ('string' != typeof param1) return {
url: param1.url,
method: param1.method || 'GET',
param: 'POST' === param1.method ? param1.data : void 0,
GMXmlHttpRequestConfig: param1
};
return {
url: param1,
method: method || 'GET',
param: body,
GMXmlHttpRequestConfig: GMXmlHttpRequestConfig || {}
};
};
const params = unifiedParameters();
if ('GET' === params.method && params.param && 'object' == typeof params.param) params.url = `${params.url}?${new URLSearchParams(params.param).toString()}`;
if ('POST' === params.method && params.param) params.GMXmlHttpRequestConfig.data = JSON.stringify(params.param);
return new Promise((resolve, reject)=>{
const newGMXmlHttpRequestConfig = {
timeout: 20000,
url: params.url,
method: params.method,
onload (response) {
if (!response.responseText) return resolve(void 0);
resolve(parseResponseText(response.responseText));
},
onerror (error) {
reject(error);
},
ontimeout () {
reject(new Error('Request timed out'));
},
headers: {
'Content-Type': 'application/json'
},
...params.GMXmlHttpRequestConfig
};
GM_xmlhttpRequest(newGMXmlHttpRequestConfig);
});
}
const originalXhrOpen = XMLHttpRequest.prototype.open;
let isHooked = false;
const hookRegistry = [];
const hookXhr = (hookUrl, callback)=>{
hookRegistry.push({
matcher: hookUrl,
callback: callback
});
if (isHooked) return;
isHooked = true;
XMLHttpRequest.prototype.open = function(method, url, async = true, username, password) {
const requestUrl = url;
const matchedHook = hookRegistry.find((h)=>h.matcher(requestUrl));
if (matchedHook) {
const descriptor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'responseText');
if (!descriptor?.get) return originalXhrOpen.call(this, method, url, async, username, password);
const getter = descriptor.get;
Object.defineProperty(this, 'responseText', {
get: ()=>{
const responseText = getter.call(this);
return matchedHook.callback(parseResponseText(responseText), requestUrl) || responseText;
}
});
}
return originalXhrOpen.call(this, method, url, async, username, password);
};
};
const returnElement = (selector, options, resolve, reject)=>{
setTimeout(()=>{
const element = options.parent.querySelector(selector);
if (!element) return void reject(new Error(`Element "${selector}" not found`));
resolve(element);
}, 1000 * options.delayPerSecond);
};
const getElementByTimer = (selector, options, resolve, reject)=>{
const intervalDelay = 100;
let intervalCounter = 0;
const maxIntervalCounter = Math.ceil(1000 * options.timeoutPerSecond / intervalDelay);
const timer = window.setInterval(()=>{
if (++intervalCounter > maxIntervalCounter) {
clearInterval(timer);
returnElement(selector, options, resolve, reject);
return;
}
const element = options.parent.querySelector(selector);
if (element) {
clearInterval(timer);
returnElement(selector, options, resolve, reject);
}
}, intervalDelay);
};
const getElementByMutationObserver = (selector, options, resolve, reject)=>{
const timer = options.timeoutPerSecond && window.setTimeout(()=>{
observer.disconnect();
reject(new Error(`Element "${selector}" not found within ${options.timeoutPerSecond} seconds`));
}, 1000 * options.timeoutPerSecond);
const observeElementCallback = (mutations)=>{
mutations.forEach((mutation)=>{
mutation.addedNodes.forEach((addNode)=>{
if (addNode.nodeType !== Node.ELEMENT_NODE) return;
const addedElement = addNode;
const element = addedElement.matches(selector) ? addedElement : addedElement.querySelector(selector);
if (element) {
timer && clearTimeout(timer);
returnElement(selector, options, resolve, reject);
}
});
});
};
const observer = new MutationObserver(observeElementCallback);
observer.observe(options.parent, {
subtree: true,
childList: true
});
return true;
};
function elementWaiter(selector, options) {
const elementWaiterOptions = {
parent: document,
timeoutPerSecond: 20,
delayPerSecond: 0.5,
...options
};
return new Promise((resolve, reject)=>{
const targetElement = elementWaiterOptions.parent.querySelector(selector);
if (targetElement) return void returnElement(selector, elementWaiterOptions, resolve, reject);
if (MutationObserver) return void getElementByMutationObserver(selector, elementWaiterOptions, resolve, reject);
getElementByTimer(selector, elementWaiterOptions, resolve, reject);
});
}
function elementGetter(selector, options) {
const elementGetterOptions = {
parent: document,
timeoutPerSecond: 20,
delayPerSecond: 0.5,
...options
};
return new Promise((resolve, reject)=>{
const targetElement = elementGetterOptions.parent.querySelector(selector);
if (targetElement) return void returnElement(selector, elementGetterOptions, resolve, reject);
getElementByTimer(selector, elementGetterOptions, resolve, reject);
});
}
const FORM_TAGS = new Set([
'INPUT',
'TEXTAREA',
'SELECT'
]);
function parseValue(targetEl, rule) {
if (!targetEl) return rule.defaultValue ?? null;
const type = rule.type || 'string';
if ('element' === type) return targetEl;
let rawValue = null;
const isFormTag = FORM_TAGS.has(targetEl.tagName);
if (rule.attribute) if (Array.isArray(rule.attribute)) for (const attr of rule.attribute){
const val = targetEl.getAttribute(attr);
if (null !== val) {
rawValue = val;
break;
}
}
else rawValue = targetEl.getAttribute(rule.attribute);
else rawValue = isFormTag ? targetEl.value : 'url' === type ? targetEl.href || targetEl.textContent : targetEl.textContent;
switch(type){
case 'number':
{
if (null === rawValue) return rule.defaultValue ?? null;
const trimmed = rawValue.trim();
if ('' === trimmed) return rule.defaultValue ?? null;
const num = Number(trimmed);
return Number.isNaN(num) ? rule.defaultValue ?? null : num;
}
case 'boolean':
if (rule.attribute) {
if (null === rawValue) return rule.defaultValue ?? false;
return 'false' !== rawValue && '0' !== rawValue;
}
{
if (isFormTag && 'checked' in targetEl) return targetEl.checked;
const str = (rawValue || '').trim().toLowerCase();
return 'true' === str || '1' === str;
}
default:
return null === rawValue ? rule.defaultValue ?? null : rawValue.trim();
}
}
function extractDOMInfo(selectorOrRoot, ruleOrRules) {
if ('string' == typeof selectorOrRoot) {
const selector = selectorOrRoot;
const rule = ruleOrRules;
const fullRule = {
...rule,
selector
};
const targetEl = document.querySelector(selector);
return {
[rule.key]: parseValue(targetEl, fullRule)
};
}
const root = selectorOrRoot;
const rules = Array.isArray(ruleOrRules) ? ruleOrRules : [
ruleOrRules
];
const result = {};
const elementCache = {};
for (const rule of rules){
if (!(rule.selector in elementCache)) elementCache[rule.selector] = root.querySelector(rule.selector);
const targetEl = elementCache[rule.selector];
result[rule.key] = parseValue(targetEl, rule);
}
return result;
}
function scroll_scroll(targetElement, container = window, scrollPercent = 0.5) {
if (!targetElement || 'number' == typeof targetElement) {
scrollPercent = targetElement || 0.5;
const yOffset = Math.round(document.body.clientHeight * scrollPercent);
window.scrollTo({
top: yOffset,
behavior: 'smooth'
});
return;
}
let containerTop = 0;
let containerHeight = document.body.clientHeight;
if (container.getBoundingClientRect) {
const rect = container.getBoundingClientRect();
containerTop = rect.top;
containerHeight = rect.height;
}
const { top: targetTop } = targetElement.getBoundingClientRect();
const yOffset = targetTop - containerTop - Math.round(containerHeight * scrollPercent);
container.scrollBy({
top: yOffset,
behavior: 'smooth'
});
}
function setValue(element, value, options) {
if (!(element instanceof HTMLInputElement) && !(element instanceof HTMLTextAreaElement)) return;
if (element.disabled || element.readOnly) return;
const prototype = element instanceof HTMLInputElement ? window.HTMLInputElement.prototype : window.HTMLTextAreaElement.prototype;
const nativeSetter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set;
if (nativeSetter) nativeSetter.call(element, value);
else element.value = value;
const { triggerFocusBlur = false } = options || {};
if (triggerFocusBlur) element.dispatchEvent(new FocusEvent('focus', {
bubbles: true
}));
element.dispatchEvent(new InputEvent('input', {
bubbles: true
}));
element.dispatchEvent(new Event('change', {
bubbles: true
}));
if (triggerFocusBlur) element.dispatchEvent(new FocusEvent('blur', {
bubbles: true
}));
}
const getButtonNumber = (button)=>{
switch(button){
case 'left':
return 0;
case 'middle':
return 1;
case 'right':
return 2;
default:
return 0;
}
};
function simulateClick(target, options) {
const { button = 'left', bubbles = true, cancelable = true, clientX = 0, clientY = 0, shiftKey = false, ctrlKey = false, altKey = false, metaKey = false, detail = 1 } = options || {};
const buttonNumber = getButtonNumber(button);
const eventInit = {
bubbles,
cancelable,
clientX,
clientY,
button: buttonNumber,
shiftKey,
ctrlKey,
altKey,
metaKey,
detail
};
const focusableElements = [
'INPUT',
'TEXTAREA',
'SELECT',
'BUTTON',
'A'
];
if (focusableElements.includes(target.tagName) || null !== target.getAttribute('tabindex')) target.focus();
const mousedownEvent = new MouseEvent('mousedown', eventInit);
target.dispatchEvent(mousedownEvent);
const clickEvent = new MouseEvent('click', eventInit);
target.dispatchEvent(clickEvent);
const mouseupEvent = new MouseEvent('mouseup', eventInit);
target.dispatchEvent(mouseupEvent);
}
const getDefaultTarget = ()=>document.activeElement instanceof HTMLElement ? document.activeElement : document.body;
function simulateKeyboard(targetOrOptions, maybeOptions) {
let target;
let options;
if (targetOrOptions instanceof HTMLElement) {
target = targetOrOptions;
options = maybeOptions || {};
} else {
target = getDefaultTarget();
options = targetOrOptions;
}
const { key = '', code = '', keyCode = 0, keyCodeValue, bubbles = true, cancelable = true, shiftKey = false, ctrlKey = false, altKey = false, metaKey = false, repeat = false } = options;
const eventInit = {
key,
code,
bubbles,
cancelable,
shiftKey,
ctrlKey,
altKey,
metaKey,
repeat
};
if (keyCode || keyCodeValue) {
Object.defineProperty(eventInit, 'keyCode', {
value: keyCodeValue || keyCode,
enumerable: true
});
Object.defineProperty(eventInit, 'which', {
value: keyCodeValue || keyCode,
enumerable: true
});
Object.defineProperty(eventInit, 'charCode', {
value: keyCodeValue || keyCode,
enumerable: true
});
}
if (target !== document.activeElement && ('INPUT' === target.tagName || 'TEXTAREA' === target.tagName || 'SELECT' === target.tagName || null !== target.getAttribute('tabindex'))) target.focus();
const keydownEvent = new KeyboardEvent('keydown', eventInit);
target.dispatchEvent(keydownEvent);
if (1 === key.length && !ctrlKey && !altKey && !metaKey) {
const keypressEvent = new KeyboardEvent('keypress', eventInit);
target.dispatchEvent(keypressEvent);
}
const keyupEvent = new KeyboardEvent('keyup', eventInit);
target.dispatchEvent(keyupEvent);
}
const isIframe = ()=>Boolean(window.frameElement && 'IFRAME' === window.frameElement.tagName || window !== window.top);
class GmStorage {
key;
defaultValue;
listenerId = null;
constructor(key, defaultValue){
this.key = key;
this.defaultValue = defaultValue;
}
get value() {
return this.get();
}
get() {
return GM_getValue(this.key, this.defaultValue);
}
set(value) {
GM_setValue(this.key, value);
}
remove() {
GM_deleteValue(this.key);
}
updateListener(callback) {
this.removeListener();
this.listenerId = GM_addValueChangeListener(this.key, (key, oldValue, newValue, remote)=>{
callback({
key,
oldValue: oldValue,
newValue: newValue,
remote
});
});
}
removeListener() {
if (null !== this.listenerId) {
GM_removeValueChangeListener(this.listenerId);
this.listenerId = null;
}
}
}
function inferDefaultValue(item) {
if (void 0 !== item.default) return item.default;
switch(item.type){
case 'number':
return 0;
case 'checkbox':
return false;
case 'text':
case 'textarea':
return '';
case 'mult-select':
return [];
case 'select':
throw new Error(`配置项 "${item.title}" 类型为 select,必须提供默认值`);
default:
throw new Error(`配置项 "${item.title}" 类型未知: ${item.type}`);
}
}
function createUserConfigStorage(userConfig) {
const result = {};
for (const [groupName, group] of Object.entries(userConfig))for (const [configKey, item] of Object.entries(group)){
const storageKey = `${groupName}.${configKey}`;
const storageName = `${configKey}Store`;
const defaultValue = inferDefaultValue(item);
result[storageName] = new GmStorage(storageKey, defaultValue);
}
return result;
}
class GmArrayStorage extends GmStorage {
constructor(key, defaultValue = []){
super(key, defaultValue);
}
get value() {
return this.get();
}
get length() {
return this.value.length;
}
get lastItem() {
const list = this.value;
return list.length > 0 ? list[list.length - 1] : void 0;
}
get firstItem() {
const list = this.value;
return list.length > 0 ? list[0] : void 0;
}
get() {
const value = super.get() ?? [];
return [
...value
];
}
set(value) {
super.set(value);
}
modify(value, index) {
this.validateIndex(index, 'modify');
const list = this.value;
list[index] = value;
this.set(list);
}
reset() {
this.set(this.defaultValue || []);
}
clear() {
this.set([]);
}
removeAt(index) {
this.validateIndex(index, 'removeAt');
const list = this.value;
list.splice(index, 1);
this.set(list);
}
delete(index) {
this.removeAt(index);
}
push(value) {
const list = this.value;
list.push(value);
this.set(list);
}
pushMany(...values) {
if (0 === values.length) return;
const list = this.value;
list.push(...values);
this.set(list);
}
pop() {
const list = this.value;
if (0 === list.length) return;
const item = list.pop();
this.set(list);
return item;
}
unshift(value) {
const list = this.value;
list.unshift(value);
this.set(list);
}
unshiftMany(...values) {
if (0 === values.length) return;
const list = this.value;
list.unshift(...values);
this.set(list);
}
shift() {
const list = this.value;
if (0 === list.length) return;
const item = list.shift();
this.set(list);
return item;
}
forEach(callback) {
this.value.forEach(callback);
}
map(callback) {
return this.value.map(callback);
}
mapInPlace(callback) {
const list = this.value;
const newList = list.map(callback);
this.set(newList);
}
filter(callback) {
return this.value.filter(callback);
}
filterInPlace(callback) {
const list = this.value;
const newList = list.filter(callback);
this.set(newList);
}
find(callback) {
return this.value.find(callback);
}
findIndex(callback) {
return this.value.findIndex(callback);
}
includes(value) {
return this.value.includes(value);
}
indexOf(value) {
return this.value.indexOf(value);
}
slice(start, end) {
return this.value.slice(start, end);
}
concat(...items) {
return this.value.concat(...items);
}
isEmpty() {
return 0 === this.value.length;
}
at(index) {
return this.value.at(index);
}
validateIndex(index, methodName) {
const length = this.value.length;
if (!Number.isInteger(index) || index < 0 || index >= length) throw new RangeError(`${methodName}: 索引 ${index} 越界,有效范围 [0, ${length - 1}]`);
}
}
class GmObjectStorage extends GmStorage {
constructor(key, defaultValue = {}){
super(key, defaultValue);
}
get value() {
return this.get();
}
get() {
const value = super.get() ?? {};
return {
...value
};
}
get size() {
return Object.keys(this.get()).length;
}
get keys() {
return Object.keys(this.get());
}
get values() {
return Object.values(this.get());
}
get entries() {
return Object.entries(this.get());
}
set(value) {
super.set(value);
}
reset() {
if (void 0 !== this.defaultValue) this.set(this.defaultValue);
else this.clear();
}
clear() {
this.set({});
}
getItem(key) {
const obj = this.get();
return obj[key];
}
setItem(key, value) {
const obj = this.get();
obj[key] = value;
this.set(obj);
}
removeItem(key) {
const obj = this.get();
delete obj[key];
this.set(obj);
}
hasItem(key) {
const obj = this.get();
return key in obj;
}
assign(partial) {
const obj = this.get();
this.set({
...obj,
...partial
});
}
pick(...keys) {
const obj = this.get();
const result = {};
for (const key of keys)if (key in obj) result[key] = obj[key];
return result;
}
omit(...keys) {
const obj = this.get();
const result = {
...obj
};
for (const key of keys)delete result[key];
return result;
}
forEach(callback) {
const obj = this.get();
for(const key in obj)if (Object.hasOwn(obj, key)) callback(obj[key], key, obj);
}
map(callback) {
const obj = this.get();
const result = {};
for(const key in obj)if (Object.hasOwn(obj, key)) result[key] = callback(obj[key], key, obj);
return result;
}
mapInPlace(callback) {
const obj = this.get();
const result = {};
for(const key in obj)if (Object.hasOwn(obj, key)) result[key] = callback(obj[key], key, obj);
this.set(result);
}
filter(callback) {
const obj = this.get();
const result = {};
for(const key in obj)if (Object.hasOwn(obj, key) && callback(obj[key], key, obj)) result[key] = obj[key];
return result;
}
filterInPlace(callback) {
const obj = this.get();
const result = {};
for(const key in obj)if (Object.hasOwn(obj, key) && callback(obj[key], key, obj)) result[key] = obj[key];
this.set(result);
}
find(callback) {
const obj = this.get();
for(const key in obj)if (Object.hasOwn(obj, key) && callback(obj[key], key, obj)) return [
key,
obj[key]
];
}
findKey(callback) {
const obj = this.get();
for(const key in obj)if (Object.hasOwn(obj, key) && callback(obj[key], key, obj)) return key;
}
some(callback) {
const obj = this.get();
for(const key in obj)if (Object.hasOwn(obj, key) && callback(obj[key], key, obj)) return true;
return false;
}
every(callback) {
const obj = this.get();
for(const key in obj)if (Object.hasOwn(obj, key) && !callback(obj[key], key, obj)) return false;
return true;
}
isEmpty() {
return 0 === this.size;
}
}
const uiImporter = (htmlContent, cssContent, options)=>{
const { isAppendCssToDocument = true, isAppendHtmlToDocument = true, appendHtmlContainer = window.document.body, isFilterScriptNode = true } = options || {};
const styleNode = cssContent && isAppendCssToDocument && GM_addStyle(cssContent) || void 0;
const domParser = new DOMParser();
const uiDoc = domParser.parseFromString(htmlContent, 'text/html');
const documentFragment = new DocumentFragment();
let nodeList = Array.from(uiDoc.body.children);
if (isFilterScriptNode) nodeList = nodeList.filter((node)=>'SCRIPT' !== node.nodeName);
documentFragment.append(...nodeList);
isAppendHtmlToDocument && appendHtmlContainer.append(documentFragment);
return {
styleNode: styleNode,
appendNodeList: nodeList
};
};
class gmMenuCommand {
static list = [];
static _renderSuspended = false;
constructor(){}
static get(title) {
const commandButton = gmMenuCommand.list.find((commandButton)=>commandButton.title === title);
if (!commandButton) throw new Error('菜单按钮不存在');
return commandButton;
}
static createToggle(details, defaultState = 'active') {
const isActiveInitially = 'active' === defaultState;
gmMenuCommand.list.push({
title: details.active.title,
onClick: ()=>{
gmMenuCommand.toggleActive(details.active.title);
gmMenuCommand.toggleActive(details.inactive.title);
details.active.onClick();
},
isActive: isActiveInitially,
id: 0
});
gmMenuCommand.list.push({
title: details.inactive.title,
onClick: ()=>{
gmMenuCommand.toggleActive(details.active.title);
gmMenuCommand.toggleActive(details.inactive.title);
details.inactive.onClick();
},
isActive: !isActiveInitially,
id: 0
});
return gmMenuCommand.render();
}
static click(title) {
const commandButton = gmMenuCommand.get(title);
commandButton.onClick();
return gmMenuCommand;
}
static create(title, onClick, isActive = true) {
if (gmMenuCommand.list.some((commandButton)=>commandButton.title === title)) throw new Error('菜单按钮已存在');
gmMenuCommand.list.push({
title,
onClick: onClick,
isActive,
id: 0
});
return gmMenuCommand.render();
}
static remove(title) {
gmMenuCommand.list = gmMenuCommand.list.filter((commandButton)=>{
const isRemove = commandButton.title !== title;
if (isRemove) gmMenuCommand.unregisterMenuCommand(commandButton.id);
return isRemove;
});
return gmMenuCommand.render();
}
static reset() {
gmMenuCommand.list.forEach(({ id })=>{
gmMenuCommand.unregisterMenuCommand(id);
});
gmMenuCommand.list = [];
return gmMenuCommand.render();
}
static batch(callback) {
gmMenuCommand._renderSuspended = true;
callback();
gmMenuCommand._renderSuspended = false;
return gmMenuCommand.render();
}
static swap(title1, title2) {
const index1 = gmMenuCommand.list.findIndex((commandButton)=>commandButton.title === title1);
const index2 = gmMenuCommand.list.findIndex((commandButton)=>commandButton.title === title2);
if (-1 === index1 || -1 === index2) throw new Error('菜单按钮不存在');
[gmMenuCommand.list[index1], gmMenuCommand.list[index2]] = [
gmMenuCommand.list[index2],
gmMenuCommand.list[index1]
];
return gmMenuCommand.render();
}
static modify(title, details) {
const commandButton = gmMenuCommand.get(title);
if (details.onClick) commandButton.onClick = details.onClick;
if (details.isActive) commandButton.isActive = details.isActive;
return gmMenuCommand.render();
}
static toggleActive(title) {
const commandButton = gmMenuCommand.get(title);
commandButton.isActive = !commandButton.isActive;
return gmMenuCommand.render();
}
static render() {
if (gmMenuCommand._renderSuspended) return gmMenuCommand;
gmMenuCommand.list.forEach((commandButton)=>{
gmMenuCommand.unregisterMenuCommand(commandButton.id);
if (commandButton.isActive) commandButton.id = GM_registerMenuCommand(commandButton.title, commandButton.onClick);
});
return gmMenuCommand;
}
static unregisterMenuCommand(id) {
GM_unregisterMenuCommand(id);
}
}
let messageContainer = null;
const activeMessages = [];
const MAX_MESSAGES = 3;
const messageTypes = {
success: {
backgroundColor: '#f0f9eb',
borderColor: '#e1f3d8',
textColor: '#67c23a',
icon: '✓'
},
warning: {
backgroundColor: '#fdf6ec',
borderColor: '#faecd8',
textColor: '#e6a23c',
icon: '⚠'
},
error: {
backgroundColor: '#fef0f0',
borderColor: '#fde2e2',
textColor: '#f56c6c',
icon: '✕'
},
info: {
backgroundColor: '#edf2fc',
borderColor: '#e4e7ed',
textColor: '#909399',
icon: 'i'
}
};
const messagePositions = {
top: {
top: '20px'
},
'top-left': {
top: '20px',
left: '20px'
},
'top-right': {
top: '20px',
right: '20px'
},
left: {
left: '20px'
},
right: {
right: '20px'
},
bottom: {
bottom: '20px'
},
'bottom-left': {
bottom: '20px',
left: '20px'
},
'bottom-right': {
bottom: '20px',
right: '20px'
}
};
const MESSAGE_STACK_CONFIG = {
GAP: 10,
BASE_OFFSET: 20
};
function calculateStackOffset(position) {
const samePositionMessages = activeMessages.filter((msg)=>msg.element.dataset.position === position);
if (0 === samePositionMessages.length) return {};
const totalOffset = samePositionMessages.reduce((acc, msg)=>acc + msg.element.offsetHeight + MESSAGE_STACK_CONFIG.GAP, 0);
const isBottom = position.includes('bottom');
if (isBottom) return {
bottom: `${MESSAGE_STACK_CONFIG.BASE_OFFSET + totalOffset}px`
};
return {
top: `${MESSAGE_STACK_CONFIG.BASE_OFFSET + totalOffset}px`
};
}
function createMessageContainer() {
if (!messageContainer) {
messageContainer = document.createElement('div');
messageContainer.setAttribute('style', `
position: fixed;
z-index: 9999999999;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
`);
document.body.appendChild(messageContainer);
}
return messageContainer;
}
function enforceMessageLimit() {
while(activeMessages.length >= MAX_MESSAGES){
const oldestMessage = activeMessages[0];
oldestMessage.close();
}
}
function getAnimationOffset(position, isEnter) {
const isBottom = position.includes('bottom');
if (isEnter) return 0;
return isBottom ? 20 : -20;
}
function validateMessageOptions(detail) {
if (!detail.message || 'string' != typeof detail.message) throw new Error('Message: message 参数必须是有效的非空字符串');
const MIN_DURATION = 100;
if (void 0 !== detail.duration) {
if ('number' != typeof detail.duration || detail.duration < MIN_DURATION) throw new Error(`Message: duration 必须是 >= ${MIN_DURATION} 的数字`);
}
const validTypes = [
'success',
'warning',
'error',
'info'
];
if (void 0 !== detail.type && !validTypes.includes(detail.type)) throw new Error(`Message: type 必须是 ${validTypes.join(' | ')} 之一`);
const validPositions = [
'top',
'top-left',
'top-right',
'left',
'right',
'bottom',
'bottom-left',
'bottom-right'
];
if (void 0 !== detail.position && !validPositions.includes(detail.position)) throw new Error(`Message: position 必须是 ${validPositions.join(' | ')} 之一`);
}
const Message = (options)=>{
const detail = {
type: 'info',
duration: 3000,
position: 'top'
};
if ('string' == typeof options) detail.message = options;
else Object.assign(detail, options);
validateMessageOptions(detail);
messageContainer = createMessageContainer();
const messageEl = document.createElement('div');
const messageType = detail.type || 'info';
const messagePosition = detail.position || 'top';
const messageDuration = detail.duration || 3000;
const typeConfig = messageTypes[messageType];
const initialOffset = getAnimationOffset(messagePosition, false);
messageEl.setAttribute('style', `
position: absolute;
min-width: 300px;
max-width: 500px;
padding: 15px 20px;
border-radius: 8px;
transform: translateY(${initialOffset}px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background-color: ${typeConfig.backgroundColor};
border: 1px solid ${typeConfig.borderColor};
color: ${typeConfig.textColor};
display: flex;
align-items: center;
transition: all 0.3s ease;
opacity: 0;
pointer-events: auto;
cursor: pointer;
${Object.entries(messagePositions[messagePosition]).map(([k, v])=>`${k}: ${v};`).join(' ')}
`);
messageEl.dataset.position = messagePosition;
enforceMessageLimit();
messageEl.setAttribute('role', 'alert');
messageEl.setAttribute('aria-live', 'polite');
messageEl.setAttribute('aria-atomic', 'true');
messageEl.setAttribute('tabindex', '0');
const iconEl = document.createElement('span');
iconEl.setAttribute('style', `
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-right: 12px;
font-size: 16px;
font-weight: bold;
`);
iconEl.textContent = typeConfig.icon;
messageEl.appendChild(iconEl);
const contentEl = document.createElement('span');
contentEl.setAttribute('style', `
flex: 1;
font-size: 14px;
line-height: 1.5;
`);
contentEl.textContent = detail.message;
messageEl.appendChild(contentEl);
messageContainer.appendChild(messageEl);
const stackOffset = calculateStackOffset(messagePosition);
if (stackOffset.top) messageEl.style.top = stackOffset.top;
if (stackOffset.bottom) messageEl.style.bottom = stackOffset.bottom;
requestAnimationFrame(()=>{
messageEl.style.opacity = '1';
messageEl.style.transform = 'translateY(0)';
});
const timer = setTimeout(()=>{
closeMessage(messageEl, messagePosition);
}, messageDuration);
messageEl.addEventListener('click', ()=>{
clearTimeout(timer);
closeMessage(messageEl, messagePosition);
});
messageEl.addEventListener('keydown', (e)=>{
if ('Escape' === e.key) {
clearTimeout(timer);
closeMessage(messageEl, messagePosition);
}
});
const close = ()=>{
clearTimeout(timer);
closeMessage(messageEl, messagePosition);
};
const messageInstance = {
close,
element: messageEl
};
activeMessages.push(messageInstance);
return messageInstance;
};
function recalculateMessagePositions() {
const positionGroups = new Map();
for (const msg of activeMessages){
const pos = msg.element.dataset.position || 'top';
if (!positionGroups.has(pos)) positionGroups.set(pos, []);
positionGroups.get(pos)?.push(msg);
}
for (const [position, messages] of positionGroups){
const isBottom = position.includes('bottom');
let currentOffset = MESSAGE_STACK_CONFIG.BASE_OFFSET;
for (const msg of messages){
if (isBottom) msg.element.style.bottom = `${currentOffset}px`;
else msg.element.style.top = `${currentOffset}px`;
currentOffset += msg.element.offsetHeight + MESSAGE_STACK_CONFIG.GAP;
}
}
}
function closeMessage(element, position = 'top') {
const index = activeMessages.findIndex((msg)=>msg.element === element);
if (-1 !== index) activeMessages.splice(index, 1);
recalculateMessagePositions();
const exitOffset = getAnimationOffset(position, false);
element.style.opacity = '0';
element.style.transform = `translateY(${exitOffset}px)`;
setTimeout(()=>{
if (element.parentNode) element.parentNode.removeChild(element);
}, 300);
}
const messageTypes_shortcuts = [
'success',
'warning',
'error',
'info'
];
messageTypes_shortcuts.forEach((type)=>{
Message[type] = (message, options)=>Message({
...options,
message,
type
});
});
function onKeydown(callback, options) {
const { target = window, once = false, capture = false, passive = false, key, ctrl = false, alt = false, shift = false, meta = false } = options || {};
const eventOptions = {
capture,
passive
};
const hasShortcutFilter = void 0 !== key || ctrl || alt || shift || meta;
let wrappedCallback;
wrappedCallback = once ? (event)=>{
if (hasShortcutFilter) {
if (void 0 !== key) {
const eventKey = event.key;
const expectedKey = key;
const isMatch = 1 === eventKey.length && 1 === expectedKey.length ? eventKey.toLowerCase() === expectedKey.toLowerCase() : eventKey === expectedKey;
if (!isMatch) return;
}
if (event.ctrlKey !== ctrl) return;
if (event.altKey !== alt) return;
if (event.shiftKey !== shift) return;
if (event.metaKey !== meta) return;
}
callback(event);
target.removeEventListener('keydown', wrappedCallback, eventOptions);
} : (event)=>{
if (hasShortcutFilter) {
if (void 0 !== key) {
const eventKey = event.key;
const expectedKey = key;
const isMatch = 1 === eventKey.length && 1 === expectedKey.length ? eventKey.toLowerCase() === expectedKey.toLowerCase() : eventKey === expectedKey;
if (!isMatch) return;
}
if (event.ctrlKey !== ctrl) return;
if (event.altKey !== alt) return;
if (event.shiftKey !== shift) return;
if (event.metaKey !== meta) return;
}
callback(event);
};
target.addEventListener('keydown', wrappedCallback, eventOptions);
return ()=>{
target.removeEventListener('keydown', wrappedCallback, eventOptions);
};
}
function onKeydownMultiple(bindings, options) {
const { target = window, capture = false, passive = false } = options || {};
const eventOptions = {
capture,
passive
};
const handleKeydown = (event)=>{
for (const binding of bindings){
const { callback, key, ctrl = false, alt = false, shift = false, meta = false } = binding;
const hasShortcutFilter = void 0 !== key || ctrl || alt || shift || meta;
if (hasShortcutFilter) {
if (void 0 !== key) {
const eventKey = event.key;
const expectedKey = key;
const isMatch = 1 === eventKey.length && 1 === expectedKey.length ? eventKey.toLowerCase() === expectedKey.toLowerCase() : eventKey === expectedKey;
if (!isMatch) continue;
}
if (event.ctrlKey !== ctrl) continue;
if (event.altKey !== alt) continue;
if (event.shiftKey !== shift) continue;
if (event.metaKey !== meta) continue;
}
callback(event);
}
};
target.addEventListener('keydown', handleKeydown, eventOptions);
return ()=>{
target.removeEventListener('keydown', handleKeydown, eventOptions);
};
}
function onKeyup(callback, options) {
const { target = window, once = false, capture = false, passive = false, key, ctrl = false, alt = false, shift = false, meta = false } = options || {};
const eventOptions = {
capture,
passive
};
const hasShortcutFilter = void 0 !== key || ctrl || alt || shift || meta;
let wrappedCallback;
wrappedCallback = once ? (event)=>{
if (hasShortcutFilter) {
if (void 0 !== key) {
const eventKey = event.key;
const expectedKey = key;
const isMatch = 1 === eventKey.length && 1 === expectedKey.length ? eventKey.toLowerCase() === expectedKey.toLowerCase() : eventKey === expectedKey;
if (!isMatch) return;
}
if (event.ctrlKey !== ctrl) return;
if (event.altKey !== alt) return;
if (event.shiftKey !== shift) return;
if (event.metaKey !== meta) return;
}
callback(event);
target.removeEventListener('keyup', wrappedCallback, eventOptions);
} : (event)=>{
if (hasShortcutFilter) {
if (void 0 !== key) {
const eventKey = event.key;
const expectedKey = key;
const isMatch = 1 === eventKey.length && 1 === expectedKey.length ? eventKey.toLowerCase() === expectedKey.toLowerCase() : eventKey === expectedKey;
if (!isMatch) return;
}
if (event.ctrlKey !== ctrl) return;
if (event.altKey !== alt) return;
if (event.shiftKey !== shift) return;
if (event.metaKey !== meta) return;
}
callback(event);
};
target.addEventListener('keyup', wrappedCallback, eventOptions);
return ()=>{
target.removeEventListener('keyup', wrappedCallback, eventOptions);
};
}
function onKeyupMultiple(bindings, options) {
const { target = window, capture = false, passive = false } = options || {};
const eventOptions = {
capture,
passive
};
const handleKeyup = (event)=>{
for (const binding of bindings){
const { callback, key, ctrl = false, alt = false, shift = false, meta = false } = binding;
const hasShortcutFilter = void 0 !== key || ctrl || alt || shift || meta;
if (hasShortcutFilter) {
if (void 0 !== key) {
const eventKey = event.key;
const expectedKey = key;
const isMatch = 1 === eventKey.length && 1 === expectedKey.length ? eventKey.toLowerCase() === expectedKey.toLowerCase() : eventKey === expectedKey;
if (!isMatch) continue;
}
if (event.ctrlKey !== ctrl) continue;
if (event.altKey !== alt) continue;
if (event.shiftKey !== shift) continue;
if (event.metaKey !== meta) continue;
}
callback(event);
}
};
target.addEventListener('keyup', handleKeyup, eventOptions);
return ()=>{
target.removeEventListener('keyup', handleKeyup, eventOptions);
};
}
let currentCallback = null;
let originalPushState = null;
let originalReplaceState = null;
let isFallbackInitialized = false;
let popstateHandler = null;
let hashchangeHandler = null;
function isNavigationSupported() {
return 'navigation' in window && window.navigation instanceof window.Navigation;
}
function triggerCallback(to, type, info, intercept, from) {
if (!currentCallback) return;
const event = {
to,
from: from ?? window.location.href,
type,
info,
intercept
};
currentCallback(event);
}
function setupNavigationApi(callback) {
currentCallback = callback;
const handleNavigate = (event)=>{
triggerCallback(event.destination.url, event.navigationType, event.info, event.canIntercept ? (handler)=>{
event.intercept({
handler
});
} : void 0);
};
window.navigation.addEventListener('navigate', handleNavigate);
return ()=>{
window.navigation.removeEventListener('navigate', handleNavigate);
currentCallback = null;
};
}
function initFallback() {
originalPushState = history.pushState;
originalReplaceState = history.replaceState;
history.pushState = function(data, unused, url) {
const fromUrl = window.location.href;
originalPushState?.call(this, data, unused, url);
const fullUrl = url ? new URL(url, fromUrl).href : window.location.href;
triggerCallback(fullUrl, 'push', void 0, void 0, fromUrl);
};
history.replaceState = function(data, unused, url) {
const fromUrl = window.location.href;
originalReplaceState?.call(this, data, unused, url);
const fullUrl = url ? new URL(url, fromUrl).href : window.location.href;
triggerCallback(fullUrl, 'replace', void 0, void 0, fromUrl);
};
popstateHandler = ()=>{
triggerCallback(window.location.href, 'traverse');
};
window.addEventListener('popstate', popstateHandler);
hashchangeHandler = ()=>{
triggerCallback(window.location.href, 'hash');
};
window.addEventListener('hashchange', hashchangeHandler);
isFallbackInitialized = true;
}
function cleanupFallback() {
if (originalPushState) {
history.pushState = originalPushState;
originalPushState = null;
}
if (originalReplaceState) {
history.replaceState = originalReplaceState;
originalReplaceState = null;
}
if (popstateHandler) {
window.removeEventListener('popstate', popstateHandler);
popstateHandler = null;
}
if (hashchangeHandler) {
window.removeEventListener('hashchange', hashchangeHandler);
hashchangeHandler = null;
}
isFallbackInitialized = false;
}
function setupFallback(callback) {
currentCallback = callback;
if (!isFallbackInitialized) initFallback();
return ()=>{
currentCallback = null;
cleanupFallback();
};
}
function onRouteChange(callback) {
if (isNavigationSupported()) return setupNavigationApi(callback);
return setupFallback(callback);
}
export { GmArrayStorage, GmObjectStorage, GmStorage, Message, createUserConfigStorage, elementGetter, elementWaiter, environmentTest, extractDOMInfo, getCookie, gmDownload, gmMenuCommand, gmRequest, hookXhr, isIframe, onKeydown, onKeydownMultiple, onKeyup, onKeyupMultiple, onRouteChange, scroll_scroll as scroll, setValue, simulateClick, simulateKeyboard, uiImporter };