diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index 2f7211447..99c2dafbd 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -19,7 +19,7 @@ export interface IGM_Base { emitEvent(event: string, eventId: string, data: any): void; } -const integrity = {}; // 僅防止非法实例化 +const integrity = {}; // 仅防止非法实例化 // GM_Base 定义内部用变量和函数。均使用@protected // 暂不考虑 Object.getOwnPropertyNames(GM_Base.prototype) 和 ts-morph 脚本生成 @@ -1026,17 +1026,25 @@ export default class GMApi extends GM_Base { } @GMContext.API({ depend: ["GM_closeInTab"], alias: "GM.openInTab" }) - public GM_openInTab(url: string, options?: GMTypes.OpenTabOptions | boolean): GMTypes.Tab { - let option: GMTypes.OpenTabOptions = {}; - if (arguments.length === 1) { - option.active = true; - } else if (typeof options === "boolean") { - option.active = !options; - } else { - option = options; + public GM_openInTab(url: string, param?: GMTypes.OpenTabOptions | boolean): GMTypes.Tab { + let option = {} as GMTypes.OpenTabOptions; + if (typeof param === "boolean") { + option.active = !param; // Greasemonkey 3.x loadInBackground + } else if (param) { + option = { ...param } as GMTypes.OpenTabOptions; + } + if (typeof option.active !== "boolean" && typeof option.loadInBackground === "boolean") { + // TM 同时兼容 active 和 loadInBackground ( active 优先 ) + option.active = !option.loadInBackground; + } else if (option.active === undefined) { + option.active = true; // TM 预设 active: false;VM 预设 active: true;旧SC 预设 active: true;GM 依从 浏览器 + } + if (option.insert === undefined) { + option.insert = true; // TM 预设 insert: true;VM 预设 insert: true;旧SC 无此设计 (false) } - if (option.active === undefined) { - option.active = true; + if (option.setParent === undefined) { + option.setParent = true; // TM 预设 setParent: false; 旧SC 预设 setParent: true; + // SC 预设 setParent: true 以避免不可预计的问题 } let tabid: any; @@ -1213,8 +1221,8 @@ export default class GMApi extends GM_Base { } } -// 從 GM_Base 對象中解構出 createGMBase 函数並導出(可供其他模塊使用) +// 从 GM_Base 对象中解构出 createGMBase 函数并导出(可供其他模块使用) export const { createGMBase } = GM_Base; -// 從 GMApi 對象中解構出內部函數,用於後續本地使用,不導出 +// 从 GMApi 对象中解构出内部函数,用于后续本地使用,不导出 const { _GM_getValue, _GM_cookie, _GM_setValue, _GM_xmlhttpRequest } = GMApi; diff --git a/src/app/service/service_worker/gm_api.ts b/src/app/service/service_worker/gm_api.ts index 7e0213a0b..a2489c40a 100644 --- a/src/app/service/service_worker/gm_api.ts +++ b/src/app/service/service_worker/gm_api.ts @@ -809,38 +809,77 @@ export default class GMApi { @PermissionVerify.API({}) async GM_openInTab(request: Request, sender: IGetSender) { - const url = request.params[0]; - const options = request.params[1] || {}; - if (options.useOpen === true) { - // 发送给offscreen页面处理 - const ok = await sendMessage(this.msgSender, "offscreen/gmApi/openInTab", { url }); - if (ok) { - // 由于window.open强制在前台打开标签,因此获取状态为{ active:true }的标签即为新标签 - const tab = await getCurrentTab(); - await cacheInstance.set(`GM_openInTab:${tab.id}`, { - uuid: request.uuid, - sender: sender.getExtMessageSender(), - }); - return tab.id; + const url = request.params[0] as string; + const options = (request.params[1] || {}) as GMTypes.OpenTabOptions & + Required>; + const getNewTabId = async () => { + if (options.useOpen === true) { + // 发送给offscreen页面处理 (使用window.open) + const ok = await sendMessage(this.msgSender, "offscreen/gmApi/openInTab", { url }); + if (ok) { + // 由于window.open强制在前台打开标签,因此获取状态为{ active:true }的标签即为新标签 + const tab = await getCurrentTab(); + return tab.id; + } else { + // 当新tab被浏览器阻止时window.open()会返回null 视为已经关闭 + // 似乎在Firefox中禁止在background页面使用window.open(),强制返回null + return false; + } } else { - // 当新tab被浏览器阻止时window.open()会返回null 视为已经关闭 - // 似乎在Firefox中禁止在background页面使用window.open(),强制返回null - return false; + const { tabId, windowId } = sender.getExtMessageSender(); + const active = options.active; + const currentTab = await chrome.tabs.get(tabId); + let newTabIndex = -1; + if (options.incognito && !currentTab.incognito) { + // incognito: "split" 在 normal 里不会看到 incognito + // 只能创建新 incognito window + // pinned 无效 + // insert 不重要 + await chrome.windows.create({ + url, + incognito: true, + focused: active, + }); + return 0; + } + if ((typeof options.insert === "number" || options.insert === true) && currentTab && currentTab.index >= 0) { + // insert 为 boolean 时,插入至当前Tab下一格 (TM行为) + // insert 为 number 时,插入至相对位置 (SC独自) + const insert = +options.insert; + newTabIndex = currentTab.index + insert; + if (newTabIndex < 0) newTabIndex = 0; + } + const createProperties = { + url, + active: active, + } as chrome.tabs.CreateProperties; + if (options.setParent) { + // SC 预设 setParent: true 以避免不可预计的问题 + createProperties.openerTabId = tabId === -1 ? undefined : tabId; + createProperties.windowId = windowId === -1 ? undefined : windowId; + } + if (options.pinned) { + // VM/FM行为 + createProperties.pinned = true; + } else if (newTabIndex >= 0) { + // insert option; pinned 情况下无效 + createProperties.index = newTabIndex; + } + const tab = await chrome.tabs.create(createProperties); + return tab.id; } - } else { - const { tabId, windowId } = sender.getExtMessageSender(); - const tab = await chrome.tabs.create({ - url, - active: options.active, - openerTabId: tabId === -1 ? undefined : tabId, - windowId: windowId === -1 ? undefined : windowId, - }); - await cacheInstance.set(`GM_openInTab:${tab.id}`, { + }; + const tabId = await getNewTabId(); + if (tabId) { + // 有 tab 创建的话 + await cacheInstance.set(`GM_openInTab:${tabId}`, { uuid: request.uuid, sender: sender.getExtMessageSender(), }); - return tab.id; + return tabId; } + // 创建失败时返回 0 + return 0; } @PermissionVerify.API({ diff --git a/src/template/scriptcat.d.tpl b/src/template/scriptcat.d.tpl index 7a997fa6a..b29342ccc 100644 --- a/src/template/scriptcat.d.tpl +++ b/src/template/scriptcat.d.tpl @@ -383,10 +383,83 @@ declare namespace GMTypes { ) => unknown; interface OpenTabOptions { + /** + * 决定新标签页是否在打开时获得焦点。 + * + * - `true` → 新标签页会立即切换到前台。 + * - `false` → 新标签页在后台打开,不会打断当前页面的焦点。 + * + * 默认值:true + */ active?: boolean; - insert?: boolean; + + /** + * 决定新标签页插入位置。 + * + * - 如果是 `boolean`: + * - `true` → 插入在当前标签页之后。 + * - `false` → 插入到窗口末尾。 + * - 如果是 `number`: + * - `0` → 插入到当前标签前一格。 + * - `1` → 插入到当前标签后一格。 + * + * 默认值:true + */ + insert?: boolean | number; + + /** + * 决定是否设置父标签页(即 `openerTabId`)。 + * + * - `true` → 浏览器能追踪由哪个标签打开的子标签, + * 有助于某些扩展(如标签树管理器)识别父子关系。 + * + * 默认值:true + */ setParent?: boolean; - useOpen?: boolean; // 这是一个实验性/不兼容其他管理器/不兼容Firefox的功能 表示使用window.open打开新窗口 #178 + + /** + * 是否在隐私窗口(无痕模式)中打开标签页。 + * + * 注意:ScriptCat 的 manifest.json 配置了 `"incognito": "split"`, + * 在 normal window 中执行时,tabId/windowId 将不可用, + * 只能执行「打开新标签页」动作。 + * + * 默认值:false + */ + incognito?: boolean; + + /** + * 历史兼容字段,仅 TM 支持。 + * 语义与 `active` **相反**: + * + * - `true` → 等价于 `active = false`(后台加载)。 + * - `false` → 等价于 `active = true`(前台加载)。 + * + * ⚠️ 不推荐使用:与 `active` 功能重复且容易混淆。 + * + * 默认值:false + * @deprecated 请使用 `active` 替代 + */ + loadInBackground?: boolean; + + /** + * 是否将新标签页固定(pin)在浏览器标签栏左侧。 + * + * - `true` → 新标签页为固定状态。 + * - `false` → 普通标签页。 + * + * 默认值:false + */ + pinned?: boolean; + + /** + * 使用 `window.open` 打开新窗口,而不是浏览器 API。 + * 可以打开某些特殊的链接 + * + * 相关:Issue #178 + * 默认值:false + */ + useOpen?: boolean; } interface XHRResponse { diff --git a/src/types/scriptcat.d.ts b/src/types/scriptcat.d.ts index 7a997fa6a..b29342ccc 100644 --- a/src/types/scriptcat.d.ts +++ b/src/types/scriptcat.d.ts @@ -383,10 +383,83 @@ declare namespace GMTypes { ) => unknown; interface OpenTabOptions { + /** + * 决定新标签页是否在打开时获得焦点。 + * + * - `true` → 新标签页会立即切换到前台。 + * - `false` → 新标签页在后台打开,不会打断当前页面的焦点。 + * + * 默认值:true + */ active?: boolean; - insert?: boolean; + + /** + * 决定新标签页插入位置。 + * + * - 如果是 `boolean`: + * - `true` → 插入在当前标签页之后。 + * - `false` → 插入到窗口末尾。 + * - 如果是 `number`: + * - `0` → 插入到当前标签前一格。 + * - `1` → 插入到当前标签后一格。 + * + * 默认值:true + */ + insert?: boolean | number; + + /** + * 决定是否设置父标签页(即 `openerTabId`)。 + * + * - `true` → 浏览器能追踪由哪个标签打开的子标签, + * 有助于某些扩展(如标签树管理器)识别父子关系。 + * + * 默认值:true + */ setParent?: boolean; - useOpen?: boolean; // 这是一个实验性/不兼容其他管理器/不兼容Firefox的功能 表示使用window.open打开新窗口 #178 + + /** + * 是否在隐私窗口(无痕模式)中打开标签页。 + * + * 注意:ScriptCat 的 manifest.json 配置了 `"incognito": "split"`, + * 在 normal window 中执行时,tabId/windowId 将不可用, + * 只能执行「打开新标签页」动作。 + * + * 默认值:false + */ + incognito?: boolean; + + /** + * 历史兼容字段,仅 TM 支持。 + * 语义与 `active` **相反**: + * + * - `true` → 等价于 `active = false`(后台加载)。 + * - `false` → 等价于 `active = true`(前台加载)。 + * + * ⚠️ 不推荐使用:与 `active` 功能重复且容易混淆。 + * + * 默认值:false + * @deprecated 请使用 `active` 替代 + */ + loadInBackground?: boolean; + + /** + * 是否将新标签页固定(pin)在浏览器标签栏左侧。 + * + * - `true` → 新标签页为固定状态。 + * - `false` → 普通标签页。 + * + * 默认值:false + */ + pinned?: boolean; + + /** + * 使用 `window.open` 打开新窗口,而不是浏览器 API。 + * 可以打开某些特殊的链接 + * + * 相关:Issue #178 + * 默认值:false + */ + useOpen?: boolean; } interface XHRResponse {