From 6abe145a8c6ff7ea5806e54122fd8d747f45b4b7 Mon Sep 17 00:00:00 2001 From: SmAsHeD <6071159+smashedr@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:05:14 -0700 Subject: [PATCH] Add Copy All Links from CTX Menu (#106) --- manifest.json | 14 ++++- package-lock.json | 8 +-- package.json | 2 +- src/html/options.html | 51 +++++++++++++----- src/js/exports.js | 70 +++++++++++++++---------- src/js/extract.js | 3 +- src/js/links.js | 1 + src/js/options.js | 48 ++++++++++++----- src/js/popup.js | 2 +- src/js/service-worker.js | 108 +++++++++++++++++++++++++++------------ tests/common.js | 1 + 11 files changed, 215 insertions(+), 93 deletions(-) diff --git a/manifest.json b/manifest.json index dabeb63..f41d766 100644 --- a/manifest.json +++ b/manifest.json @@ -12,11 +12,23 @@ }, "description": "Show Main Popup Action" }, - "extract": { + "extractAll": { "suggested_key": { "default": "Alt+Shift+X" }, "description": "Extract Links from Tab(s)" + }, + "extractSelection": { + "description": "Extract Links from Selected Text" + }, + "copyAll": { + "suggested_key": { + "default": "Alt+Shift+C" + }, + "description": "Copy Links from Tab(s)" + }, + "copySelection": { + "description": "Copy Links from Selected Text" } }, "omnibox": { diff --git a/package-lock.json b/package-lock.json index 1c588eb..6cf9096 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "pdfjs-dist": "^4.7.76" }, "devDependencies": { - "@types/chrome": "^0.0.277", + "@types/chrome": "^0.0.278", "eslint": "^8.57.0", "gulp": "^4.0.2", "json-merger": "^1.1.10", @@ -553,9 +553,9 @@ "license": "MIT" }, "node_modules/@types/chrome": { - "version": "0.0.277", - "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.277.tgz", - "integrity": "sha512-qoTgBcDWblSsX+jvFnpUlLUE3LAuOhZfBh9MyMWMQHDsQiYVgBvdZWu9COrdB9+aNnInEyXcFgfc2HE16sdSYQ==", + "version": "0.0.278", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.278.tgz", + "integrity": "sha512-PDIJodOu7o54PpSOYLybPW/MDZBCjM1TKgf31I3Q/qaEbNpIH09rOM3tSEH3N7Q+FAqb1933LhF8ksUPYeQLNg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1a28c5c..b7f0085 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "pdfjs-dist": "^4.7.76" }, "devDependencies": { - "@types/chrome": "^0.0.277", + "@types/chrome": "^0.0.278", "eslint": "^8.57.0", "gulp": "^4.0.2", "json-merger": "^1.1.10", diff --git a/src/html/options.html b/src/html/options.html index 334abc3..9f53844 100644 --- a/src/html/options.html +++ b/src/html/options.html @@ -26,17 +26,36 @@

Link Extractor

v

- - - - - - - - - - -
Keyboard Shortcuts
DescriptionShortcut
Unknown
+
+
+
+ Keyboard Shortcuts +
+
+ + + + + + + + + + +
Keyboard Shortcuts
DescriptionShortcut
Unknown
+
+ Manage Keyboard Shortcuts: + + https://mzl.la/3Qwp5QQ + chrome://extensions/shortcuts +
+
+ +
+
+ General Options +
+
@@ -48,7 +67,7 @@

Link Extractor

Regex Flags for Filtering. - More Info + More Info
@@ -115,7 +134,7 @@

Link Extractor

data-bs-toggle="tooltip" data-bs-placement="top" data-bs-trigger="hover" data-bs-title="Allow Extracting Links from Multiple Selected Tabs."> Grant Host Permissions - More about Permissions + More about Permissions
-
+
+
+ Saved Filters +
+
diff --git a/src/js/exports.js b/src/js/exports.js index 79919d0..e3d740e 100644 --- a/src/js/exports.js +++ b/src/js/exports.js @@ -4,39 +4,37 @@ export const githubURL = 'https://github.com/cssnr/link-extractor' /** * Inject extract.js to Tab and Open links.html with params - * @function processLinks + * @function injectTab * @param {Object} injectOptions Inject Tab Options * @param {String} [injectOptions.filter] Regex Filter * @param {Boolean} [injectOptions.domains] Only Domains * @param {Boolean} [injectOptions.selection] Only Selection + * @param {Boolean} [injectOptions.open] Open Links Page + * @param {chrome.tabs.Tab} [injectOptions.tab] Open Links Page * @return {Promise} */ export async function injectTab({ filter = null, domains = false, selection = false, + open = true, + tab = null, } = {}) { console.log('injectTab:', filter, domains, selection) // Extract tabIds from all highlighted tabs const tabIds = [] - const tabs = await chrome.tabs.query({ - currentWindow: true, - highlighted: true, - }) - if (!tabs.length) { - const [tab] = await chrome.tabs.query({ - currentWindow: true, - active: true, - }) - console.debug(`tab: ${tab.id}`, tab) + if (tab) { tabIds.push(tab.id) } else { + const tabs = await chrome.tabs.query({ + currentWindow: true, + highlighted: true, + }) + console.debug('tabs:', tabs) for (const tab of tabs) { console.debug(`tab: ${tab.id}`, tab) - // tab.url undefined means we do not have permissions on this tab if (!tab.url) { - // chrome.runtime.openOptionsPage() const url = new URL( chrome.runtime.getURL('/html/permissions.html') ) @@ -49,15 +47,28 @@ export async function injectTab({ tabIds.push(tab.id) } } + console.log('tabIds:', tabIds) if (!tabIds.length) { - console.log('%cNo Tab IDs to Inject', 'color: Yellow') + // TODO: Display Error to User + console.error('No Tab IDs to Inject') return } - console.log('tabIds:', tabIds) - // Create URL to links.html - const url = new URL(chrome.runtime.getURL('/html/links.html')) + // Inject extract.js which listens for messages + for (const tab of tabIds) { + console.debug(`injecting tab.id: ${tab}`) + await chrome.scripting.executeScript({ + target: { tabId: tab }, + files: ['/js/extract.js'], + }) + } + // Create URL to links.html if open + if (!open) { + console.debug('Skipping opening links.html on !open:', open) + return + } + const url = new URL(chrome.runtime.getURL('/html/links.html')) // Set URL searchParams url.searchParams.set('tabs', tabIds.join(',')) if (filter) { @@ -69,16 +80,6 @@ export async function injectTab({ if (selection) { url.searchParams.set('selection', selection.toString()) } - - // Inject extract.js which listens for messages - for (const tab of tabIds) { - console.debug(`injecting tab.id: ${tab}`) - await chrome.scripting.executeScript({ - target: { tabId: tab }, - files: ['/js/extract.js'], - }) - } - // Open Tab to links.html with desired params console.debug(`url: ${url.href}`) await chrome.tabs.create({ active: true, url: url.href }) @@ -394,3 +395,18 @@ export function detectBrowser() { } return browser } + +/** + * @function updateBrowser + * @return {Promise} + */ +export function updateBrowser() { + let selector = '.chrome' + // noinspection JSUnresolvedReference + if (typeof browser !== 'undefined') { + selector = '.firefox' + } + document + .querySelectorAll(selector) + .forEach((el) => el.classList.remove('d-none')) +} diff --git a/src/js/extract.js b/src/js/extract.js index ffdd279..328adc8 100644 --- a/src/js/extract.js +++ b/src/js/extract.js @@ -95,7 +95,8 @@ function extractSelection() { if (ancestor.nodeName === '#text') { continue } - ancestor.querySelectorAll('a, area').forEach((el) => { + // console.debug('ancestor:', ancestor) + ancestor?.querySelectorAll('a, area')?.forEach((el) => { if (selection.containsNode(el, true)) { // console.debug('el:', el) pushElement(links, el) diff --git a/src/js/links.js b/src/js/links.js index d2bddff..d4ee926 100644 --- a/src/js/links.js +++ b/src/js/links.js @@ -544,6 +544,7 @@ function handleKeyboard(e) { input?.focus() input?.select() } else if (['KeyT', 'KeyO'].includes(e.code)) { + // noinspection JSIgnoredPromiseFromCall chrome.runtime.openOptionsPage() } } diff --git a/src/js/options.js b/src/js/options.js index 823fba1..9342815 100644 --- a/src/js/options.js +++ b/src/js/options.js @@ -10,6 +10,7 @@ import { onRemoved, revokePerms, saveOptions, + updateBrowser, updateManifest, updateOptions, } from './exports.js' @@ -33,15 +34,20 @@ document document .querySelectorAll('.grant-permissions') .forEach((el) => el.addEventListener('click', grantPerms)) -document - .getElementById('options-form') - .addEventListener('submit', (e) => e.preventDefault()) document .querySelectorAll('#options-form input, select') .forEach((el) => el.addEventListener('change', saveOptions)) +document + .getElementById('options-form') + .addEventListener('submit', (e) => e.preventDefault()) document .querySelectorAll('[data-bs-toggle="tooltip"]') .forEach((el) => new bootstrap.Tooltip(el)) +document + .getElementById('chrome-shortcuts') + ?.addEventListener('click', () => + chrome.tabs.update({ url: 'chrome://extensions/shortcuts' }) + ) document.getElementById('export-data').addEventListener('click', exportClick) document.getElementById('import-data').addEventListener('click', importClick) @@ -60,7 +66,15 @@ async function initOptions() { // noinspection ES6MissingAwait updateManifest() // noinspection ES6MissingAwait - setShortcuts() + updateBrowser() + // noinspection ES6MissingAwait + setShortcuts([ + '_execute_action', + 'extractAll', + 'extractSelection', + 'copyAll', + 'copySelection', + ]) // noinspection ES6MissingAwait checkPerms() chrome.storage.sync.get(['options', 'patterns']).then((items) => { @@ -419,24 +433,34 @@ function beginEditing(event, idx) { /** * Set Keyboard Shortcuts * @function setShortcuts - * @param {String} selector + * @param {Array} names + * @param {String} [selector] + * @return {Promise} */ -async function setShortcuts(selector = '#keyboard-shortcuts') { +async function setShortcuts(names, selector = '#keyboard-shortcuts') { if (!chrome.commands) { return console.debug('Skipping: chrome.commands') } - const table = document.querySelector(selector) - table.classList.remove('d-none') + const parent = document.querySelector(selector) + parent.classList.remove('d-none') + const table = parent.querySelector('table') + console.log('table:', table) const tbody = table.querySelector('tbody') const source = table.querySelector('tfoot > tr').cloneNode(true) + // console.log('source:', source) const commands = await chrome.commands.getAll() - for (const command of commands) { - // console.debug('command:', command) + // console.log('commands:', commands) + for (const name of names) { + const command = commands.find((x) => x.name === name) + console.debug('command:', command) + if (!command) { + console.warn('Command Not Found:', command) + } const row = source.cloneNode(true) - // TODO: Chrome does not parse the description for _execute_action in manifest.json let description = command.description + // Note: Chrome does not parse the description for _execute_action in manifest.json if (!description && command.name === '_execute_action') { - description = 'Show Main Popup Action' + description = 'Show Popup Action' } row.querySelector('.description').textContent = description row.querySelector('kbd').textContent = command.shortcut || 'Not Set' diff --git a/src/js/popup.js b/src/js/popup.js index 3312829..8168aaa 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -169,7 +169,7 @@ async function popupLinks(event) { console.debug('href:', href) let url if (href.endsWith('html/options.html')) { - chrome.runtime.openOptionsPage() + await chrome.runtime.openOptionsPage() window.close() return } else if (href === '#') { diff --git a/src/js/service-worker.js b/src/js/service-worker.js index 387ddbc..9278fc4 100644 --- a/src/js/service-worker.js +++ b/src/js/service-worker.js @@ -37,6 +37,7 @@ async function onInstalled(details) { createContextMenus(patterns) } if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) { + // noinspection ES6MissingAwait chrome.runtime.openOptionsPage() await chrome.tabs.create({ active: false, url: installURL }) } else if (details.reason === chrome.runtime.OnInstalledReason.UPDATE) { @@ -49,7 +50,6 @@ async function onInstalled(details) { } } } - setUninstallURL() checkPerms().then((hasPerms) => { if (hasPerms) { onAdded() @@ -57,6 +57,7 @@ async function onInstalled(details) { onRemoved() } }) + setUninstallURL() } /** @@ -97,7 +98,7 @@ function setUninstallURL() { async function onClicked(ctx, tab) { console.log('onClicked:', ctx, tab) if (['options', 'filters'].includes(ctx.menuItemId)) { - chrome.runtime.openOptionsPage() + await chrome.runtime.openOptionsPage() } else if (ctx.menuItemId === 'links') { console.debug('injectTab: links') await injectTab() @@ -106,7 +107,7 @@ async function onClicked(ctx, tab) { await injectTab({ domains: true }) } else if (ctx.menuItemId === 'selection') { console.debug('injectTab: selection') - await injectTab({ selection: true }) + await injectTab({ tab, selection: true }) } else if (ctx.menuItemId.startsWith('filter-')) { const i = ctx.menuItemId.split('-')[1] console.debug(`injectTab: filter-${i}`) @@ -114,16 +115,20 @@ async function onClicked(ctx, tab) { console.debug(`filter: ${patterns[i]}`) await injectTab({ filter: patterns[i] }) } else if (ctx.menuItemId === 'copy') { - console.debug('injectFunction: copy: copyActiveElementText') + console.debug('injectFunction: copyActiveElementText: copy', ctx) await injectFunction(copyActiveElementText, [ctx]) - } else if (ctx.menuItemId === 'copyLinks') { - console.debug('injectFunction: copyLinks: copySelectionLinks', tab) - await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - files: ['/js/extract.js'], - }) + } else if (ctx.menuItemId === 'copyAllLinks') { + console.debug('injectFunction: copyLinks: copyAllLinks', tab) + // await injectCopyLinks(tab) + await injectTab({ tab, open: false }) + const { options } = await chrome.storage.sync.get(['options']) + await injectFunction(copyLinks, [options.removeDuplicates]) + } else if (ctx.menuItemId === 'copySelLinks') { + console.debug('injectFunction: copyLinks: copySelLinks', tab) + // await injectCopyLinks(tab, true) + await injectTab({ tab, open: false }) const { options } = await chrome.storage.sync.get(['options']) - await injectFunction(copySelectionLinks, [options.removeDuplicates]) + await injectFunction(copyLinks, [options.removeDuplicates, true]) } else { console.error(`Unknown ctx.menuItemId: ${ctx.menuItemId}`) } @@ -133,11 +138,28 @@ async function onClicked(ctx, tab) { * On Command Callback * @function onCommand * @param {String} command + * @param {chrome.tabs.Tab} tab */ -async function onCommand(command) { - console.log('onCommand:', command) - if (command === 'extract') { +async function onCommand(command, tab) { + console.log(`onCommand: ${command}:`, tab) + if (command === 'extractAll') { + console.debug('extractAll') await injectTab() + } else if (command === 'extractSelection') { + console.debug('extractSelection') + await injectTab({ selection: true }) + } else if (command === 'copyAll') { + console.debug('copyAll') + // await injectCopyLinks(tab) + await injectTab({ open: false }) + const { options } = await chrome.storage.sync.get(['options']) + await injectFunction(copyLinks, [options.removeDuplicates, true]) + } else if (command === 'copySelection') { + console.debug('copySelection') + // await injectCopyLinks(tab, true) + await injectTab({ open: false }) + const { options } = await chrome.storage.sync.get(['options']) + await injectFunction(copyLinks, [options.removeDuplicates, true]) } else { console.error(`Unknown command: ${command}`) } @@ -254,13 +276,14 @@ function createContextMenus(patterns) { chrome.contextMenus.removeAll() const contexts = [ [['link'], 'copy', 'Copy Link Text to Clipboard'], - [['selection'], 'copyLinks', 'Copy Selected Links to Clipboard'], + [['all'], 'copyAllLinks', 'Copy All Links to Clipboard'], + [['selection'], 'copySelLinks', 'Copy Selected Links to Clipboard'], [['selection'], 'selection', 'Extract Links from Selection'], - [['selection', 'link'], 'separator'], - [['all'], 'filters', 'Extract with Filter'], + [['all'], 'separator'], [['all'], 'links', 'Extract All Links'], - [['all'], 'domains', 'Extract All Domains'], - [['selection', 'link'], 'separator'], + [['all'], 'filters', 'Extract with Filter'], + [['all'], 'domains', 'Extract Domains Only'], + [['all'], 'separator'], [['all'], 'options', 'Open Options'], ] contexts.forEach(addContext) @@ -280,23 +303,25 @@ function createContextMenus(patterns) { /** * Add Context from Array * @function addContext - * @param {[[ContextType],String,String,String]} context + * @param {[chrome.contextMenus.ContextType[],String,String,chrome.contextMenus.ContextItemType?]} context */ function addContext(context) { + // console.debug('addContext:', context) try { - // console.debug('addContext:', context) if (context[1] === 'separator') { - context[1] = Math.random().toString().substring(2, 7) + const id = Math.random().toString().substring(2, 7) + context[1] = `${id}` context.push('separator', 'separator') } + // console.debug('menus.create:', context) chrome.contextMenus.create({ contexts: context[0], id: context[1], title: context[2], - type: context[3], + type: context[3] || 'normal', }) } catch (e) { - console.warn(`Error Adding Context: ${e.message}`, e) + console.log('%cError Adding Context:', 'color: Yellow', e) } } @@ -327,14 +352,20 @@ function copyActiveElementText(ctx) { } /** - * Copy All Selected Links + * Copy All Links * @function copySelectionLinks * @param {Boolean} removeDuplicates + * @param {Boolean} selection */ -function copySelectionLinks(removeDuplicates) { - // console.debug('copySelectionLinks:', removeDuplicates) - const links = extractSelection() - // console.debug('links:', links) +function copyLinks(removeDuplicates, selection = false) { + console.debug('copyLinks:', removeDuplicates, selection) + let links + if (selection) { + links = extractSelection() + } else { + links = extractAllLinks() + } + console.debug('links:', links) let results = [] for (const link of links) { results.push(link.href) @@ -353,19 +384,32 @@ function copySelectionLinks(removeDuplicates) { } } +// async function injectCopyLinks(tab, selection = false) { +// console.debug('copySelection') +// await chrome.scripting.executeScript({ +// target: { tabId: tab.id }, +// files: ['/js/extract.js'], +// }) +// const { options } = await chrome.storage.sync.get(['options']) +// await injectFunction(copyLinks, [options.removeDuplicates, selection]) +// } + /** * Inject Function into Current Tab with args * @function injectFunction * @param {Function} func * @param {Array} args + * @return {Promise<*>} */ async function injectFunction(func, args) { const [tab] = await chrome.tabs.query({ currentWindow: true, active: true }) - await chrome.scripting.executeScript({ + const results = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: func, args: args, }) + console.log('results:', results) + return results[0]?.result } /** @@ -375,7 +419,7 @@ async function injectFunction(func, args) { * @return {Promise} */ async function setDefaultOptions(defaultOptions) { - console.log('setDefaultOptions', defaultOptions) + console.log('setDefaultOptions:', defaultOptions) let { options, patterns } = await chrome.storage.sync.get([ 'options', 'patterns', @@ -402,7 +446,7 @@ async function setDefaultOptions(defaultOptions) { } if (changed) { await chrome.storage.sync.set({ options }) - console.debug('changed:', options) + console.debug('changed options:', options) } return { options, patterns } diff --git a/tests/common.js b/tests/common.js index caaf9f5..306a8a0 100644 --- a/tests/common.js +++ b/tests/common.js @@ -15,6 +15,7 @@ async function getBrowser() { args: [ `--disable-extensions-except=${pathToExtension}`, `--load-extension=${pathToExtension}`, + '--no-sandbox', // '--disable-blink-features=AutomationControlled', // '--disable-features=ChromeUserPermPrompt', ],