UNPKG

@yiero/gmlib

Version:

GM Lib for Tampermonkey/ScriptCat

1,372 lines (1,371 loc) 48.5 kB
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 };